/************************************************************************************
 * Dual-PID Controller Firmware (Zeitproportional)
 * Version (Siehe unten im Code)
 *
 * Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler
 * mit zeitproportionaler Ansteuerung der SSRs.
 * Sensoren: MAX6675 (Dampf), NTC (Wasser).
 * Funktionen: Eco-Modus, Profile, Fast-Heat-Up, WLAN-Weboberfläche, AutoTune,
 * Wartungserinnerung, Werkseinstellungen, Hostname-Konfiguration.
 *
 * Compiler- und Hardware-Umgebung:
 * - ESP8266: Wemos D1 mini, 4MB
 * - Bibliotheken: Wire, Adafruit_GFX, Adafruit_SH110X, EEPROM, PID_v1, PID_AutoTune_v0,
 * max6675, ESP8266WiFi, ESP8266WebServer, ESP8266mDNS
 ************************************************************************************/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <EEPROM.h>
#include <PID_v1.h>
#include <PID_AutoTune_v0.h> // Modifizierte AutoTune-Funktion - Muss als .zip eingebunden werden
#include "max6675.h"
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <math.h>                 // Für isnan()
#include <FS.h>                   // Dateisystem-Basis
#include <LittleFS.h>             // LittleFS-Implementierung

/************************************************************************************
 * Hardware-Pinbelegung
 ************************************************************************************/

#define SSR_WASSER_PIN D2         // SSR für Wasser-Heizung
#define SSR_DAMPF_PIN D1          // SSR für Dampf-Heizung
#define SHOT_TIMER_PIN D8         // Input für Shot-Timer
#define OLED_SDA D3               // SDA-Leitung für OLED
#define OLED_SCK D4               // SCK-Leitung für OLED
#define ANALOG_NTC_PIN A0         // Analoger Eingang für den NTC-Sensor

// Display-Objekt (Adafruit SH1106G)
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);

/************************************************************************************
 * MAX6675 (Dampf-Temperatursensor)
 ************************************************************************************/

#define MAX6675_SCK D7            // Serial Clock
#define MAX6675_CS  D6            // Chip Select
#define MAX6675_SO  D5            // Serial Out
MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);

/************************************************************************************
 * Firmware-Informationen
 ************************************************************************************/

String version = "2.9.0";
String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail =
  "<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
String versionHerstellerWeb =
  "<a href='https://raw-designs.de/' target='_blank' class='info-link'>https://raw-designs.de/</a> | "
  "<a href='https://mueller.black/' target='_blank' class='info-link'>https://mueller.black</a>";

// Demo-Modus: Hier können bestimmte Funktionen (WLAN-Anpassungen etc.) eingeschränkt werden.
// Entweder hier pauschal auf true setzen oder über IP-Adresse im Setup()-Teil auf true setzen lassen.
bool demoModus = false;
IPAddress demoModusIP(192, 168, 178, 67);

// Dieses Kennwort muss in einer Firmware-Binärdatei vorhanden sein, damit ein Update gültig ist.
static const char FIRMWARE_PASSWORD[] = "FWKennwort123";
static bool passwordFound = false;
static int passMatchPos = 0;
const int passLength = sizeof(FIRMWARE_PASSWORD) - 1;

/************************************************************************************
 * Geräte-Infos
 ************************************************************************************/

char infoHersteller[50];
char infoModell[50];
char infoZusatz[50];

/************************************************************************************
 * NTC-Sensor-Parameter (Wasser)
 * - A0 wird verwendet, um die Temperatur über einen Spannungsteiler zu messen
 ************************************************************************************/

const double V_SUPPLY = 3.3;      // Versorgungsspannung
const double ADC_REF  = 3.3;      // ADC-Referenz
const double R_FIXED  = 3000.0;   // Festwiderstand 3 kΩ

// Daten des NTC
const double T0    = 298.15;      // Referenztemperatur in Kelvin (25°C)
const double R0    = 50000.0;     // NTC-Widerstand bei 25°C
const double beta  = 3950.0;      // Beta-Wert

/************************************************************************************
 * Temperatur-Profil-Struktur
 * - Speichert Wasser- und Dampf-Setpoint, Eco-Einstellungen und Profilname
 ************************************************************************************/

struct TemperatureProfile
{
  double setpointWasser;
  double setpointDampf;
  int ecoModeMinutes;
  bool dynamicEcoActive;
  int dampfVerzoegerung;
  char profileName[20];
};

/************************************************************************************
 * PID-Regler-Konfiguration & Zeitproportionale Steuerung
 ************************************************************************************/

// PID Input/Output/Setpoint Variablen
double SetpointDampf, InputDampf, OutputDampf;
double SetpointWasser, InputWasser, OutputWasser;
double OffsetDampf = 0.0, OffsetWasser = 0.0;

// PID-Konstanten (Regelungsparameter)
double KpDampf = 3.0,  KiDampf = 0.1,  KdDampf = 0.5;
double KpWasser = 5.0, KiWasser = 0.2, KdWasser = 1.0;

// Zeitproportionale Steuerung
unsigned long windowSizeWasser; // Fenstergröße für Wasser-SSR (in ms)
unsigned long windowSizeDampf;  // Fenstergröße für Dampf-SSR (in ms)
unsigned long windowStartTimeWasser;   // Startzeit des aktuellen Fensters für Wasser
unsigned long windowStartTimeDampf;    // Startzeit des aktuellen Fensters für Dampf

// PID-Regler Instanzen
// WICHTIG: Die Output-Grenzen werden in setup() auf die Fenstergröße gesetzt!
PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT);
PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT);

// Variablen für Sicherheitsabschaltung
double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded
double maxTempDampf  = 180.0; // Standardwert, wie bisher hardcoded

/************************************************************************************
 * Standardwerte und Default-Einstellungen
 ************************************************************************************/

double defaultSetpointDampf     = 165.0;
double defaultSetpointWasser    = 93.0;
double defaultOffsetDampf       = 0.0;
double defaultOffsetWasser      = 0.0;
double defaultKpDampf           = 3.0;
double defaultKiDampf           = 0.1;
double defaultKdDampf           = 0.5;
double defaultKpWasser          = 5.0;
double defaultKiWasser          = 0.2;
double defaultKdWasser          = 1.0;
int defaultEcoModeMinutes       = 0;
int defaultEcoModeTempWasser    = 60;
int defaultEcoModeTempDampf     = 60;
String defaultInfoHersteller    = "";
String defaultInfoModell        = "";
String defaultInfoZusatz        = "";
int defaultDampfVerzoegerung    = 0;
int dampfVerzoegerung           = 0; 
int defaultMaintenanceInterval  = 0;
String defaultHostname          = "Dual-PID-Controller";
unsigned long defaultWindowSizeWasser = 2000;
unsigned long defaultWindowSizeDampf  = 2000;

/************************************************************************************
 * Webserver und zugehörige Objekte
 ************************************************************************************/

ESP8266WebServer server(80); // Webserver auf Port 80

// MagicValue zum Erkennen bereits gespeicherter EEPROM-Daten
const char storageMagicValue[5] = "MGVE";

/************************************************************************************
 * EEPROM Adressen für das Speichern/Laden von Werten
 ************************************************************************************/
const int EEPROM_SIZE = 1024; // Gesamtgröße des verwendeten EEPROM-Speichers

/************************************************************************************
 * EEPROM Adressen für das Speichern/Laden von Werten
 ************************************************************************************/

const int EEPROM_ADDR_MAGICVALUE           = 0;    // 5 Bytes (char[5])
const int EEPROM_ADDR_SETPOINT_DAMPF       = 5;    // 8 Bytes (double)
const int EEPROM_ADDR_SETPOINT_WASSER      = 13;   // 8 Bytes
const int EEPROM_ADDR_OFFSET_DAMPF         = 21;   // 8 Bytes
const int EEPROM_ADDR_OFFSET_WASSER        = 29;   // 8 Bytes
const int EEPROM_ADDR_KP_DAMPF             = 37;   // 8 Bytes
const int EEPROM_ADDR_KI_DAMPF             = 45;   // 8 Bytes
const int EEPROM_ADDR_KD_DAMPF             = 53;   // 8 Bytes
const int EEPROM_ADDR_KP_WASSER            = 61;   // 8 Bytes
const int EEPROM_ADDR_KI_WASSER            = 69;   // 8 Bytes
const int EEPROM_ADDR_KD_WASSER            = 77;   // 8 Bytes
const int EEPROM_ADDR_ECOMODE_MINUTES      = 85;   // 4 Bytes (int)
const int EEPROM_ADDR_ECOMODE_TEMP_WASSER  = 89;   // 4 Bytes
const int EEPROM_ADDR_ECOMODE_TEMP_DAMPF   = 93;   // 4 Bytes
const int EEPROM_ADDR_INFO_HERSTELLER      = 97;   // 50 Bytes (char[50])
const int EEPROM_ADDR_INFO_MODELL          = 147;  // 50 Bytes
const int EEPROM_ADDR_INFO_ZUSATZ          = 197;  // 50 Bytes
const int EEPROM_ADDR_FASTHEATUP_DATA      = 247;  // 1 Byte (bool)
const int EEPROM_ADDR_RUNTIME              = 248;  // 4 Bytes (unsigned long)
const int EEPROM_ADDR_SHOTCOUNTER          = 252;  // 4 Bytes (unsigned long)
const int EEPROM_ADDR_DYNAMIC_ECO_MODE     = 256;  // 1 Byte (bool)
const int EEPROM_ADDR_PROFILE_1            = 257;  // 50 Bytes für Profil 1 (struct TemperatureProfile)
const int EEPROM_ADDR_PROFILE_2            = 307;  // 50 Bytes für Profil 2 (struct TemperatureProfile)
// --- AutoTune Parameter Wasser ---
const int EEPROM_ADDR_TUNING_STEP_WASSER            = 357;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_NOISE_WASSER           = 365;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_STARTVALUE_WASSER      = 373;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_LOOKBACK_WASSER        = 381;   // 4 Bytes (unsigned int) -> Ende 384
// --- WiFi Konfiguration ---
const int EEPROM_ADDR_WIFI_CONFIG_MAGIC             = 385;   // 5 Bytes (char[5])
const int EEPROM_ADDR_WIFI_CONFIG_DATA              = 390;   // 113 Bytes (struct WiFiConfig) -> Ende ca. 503
// --- Sonstige Einstellungen ---
const int EEPROM_ADDR_STEAM_DELAY                   = 527;   // 4 Bytes (int) -> Ende 530
// --- AutoTune Parameter Dampf ---
const int EEPROM_ADDR_TUNING_STEP_DAMPF             = 531;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_NOISE_DAMPF            = 539;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_STARTVALUE_DAMPF       = 547;   // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_LOOKBACK_DAMPF         = 555;   // 4 Bytes (unsigned int) -> Ende 558
// --- Adressen für Übertemperatursicherung / Sicherheitsabschaltung ---
const int EEPROM_ADDR_MAX_TEMP_WASSER               = 559;   // 8 Bytes (double) -> Ende 566
const int EEPROM_ADDR_MAX_TEMP_DAMPF                = 567;   // 8 Bytes (double) -> Ende 574
// --- NEUE ADRESSEN ---
const int EEPROM_ADDR_MAINTENANCE_INTERVAL          = 575;   // 4 Bytes (int) -> Ende 578
const int EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER  = 579;   // 4 Bytes (int) -> Ende 578
const int EEPROM_ADDR_HOSTNAME                      = 583;   // 33 Bytes (char[33]) -> Ende 611
// --- Boost-Funktion
const int EEPROM_ADDR_BOOST_WASSER_ACTIVE           = 612;   // 1 Byte (bool) -> Ende 612
const int EEPROM_ADDR_BOOST_DAMPF_ACTIVE            = 613;   // 1 Byte (bool) -> Ende 613
// --- Zeitfenster-Größen für Dampf und Wasser
const int EEPROM_ADDR_WINDOWSIZE_WASSER             = 614;   // 4 Bytes (unsigned long) -> Ende 617
const int EEPROM_ADDR_WINDOWSIZE_DAMPF              = 618;   // 4 Bytes (unsigned long) -> Ende 621
// --- Heiz-Begrenzung oberhalb Setpoints
const int EEPROM_ADDR_PREVENTHEAT_WASSER            = 622;   // 1 Byte (bool) -> Ende 622
const int EEPROM_ADDR_PREVENTHEAT_DAMPF             = 623;   // 1 Byte (bool) -> Ende 623


/************************************************************************************
 * Eco-Mode Variablen
 ************************************************************************************/

unsigned long lastShotTime = 0;    // Wann wurde zuletzt ein Shot beendet
int ecoModeMinutes         = 0;    // Zeit (Minuten) bis Eco-Modus
bool ecoModeAktiv          = 0;    // Flag, ob Eco läuft
int ecoModeTempWasser      = 50;   // Zieltemp Wasser im Eco
int ecoModeTempDampf       = 50;   // Zieltemp Dampf im Eco
bool dynamicEcoActive      = false;
unsigned long ecoModeActivatedTime = 0;

/************************************************************************************
 * Fast-Heat-Up Variablen
 ************************************************************************************/

bool fastHeatUpAktiv        = 0;    // Ist Fast-Heat-Up aktiviert?
bool fastHeatUpHeating      = 0;    // Wird gerade hochgeheizt (Wasser >130)?
int fastHeatUpSetpoint      = 130;

/************************************************************************************
 * Sicherheits- und Fehlerstatus
 ************************************************************************************/

bool wasserSensorError      = false;  // Flag für Fehler am Wassertemperatursensor
bool dampfSensorError       = false;  // Flag für Fehler am Dampftemperatursensor
bool wasserSafetyShutdown   = false;
bool dampfSafetyShutdown    = false;

/************************************************************************************
 * Auto-Tune-Einstellungen
 ************************************************************************************/

// Parameter für Wasser
double tuningStepWasser;
double tuningNoiseWasser;
double tuningStartValueWasser;
unsigned int tuningLookBackWasser;
const double defaultTuningStepWasser      = 500.0; 
const double defaultTuningNoiseWasser     = 1.0;
const double defaultTuningStartValueWasser = 1000.0;
const unsigned int defaultTuningLookBackWasser = 60;

// Parameter für Dampf
double tuningStepDampf;
double tuningNoiseDampf;
double tuningStartValueDampf;
unsigned int tuningLookBackDampf;
const double defaultTuningStepDampf       = 300.0; 
const double defaultTuningNoiseDampf      = 1.0;
const double defaultTuningStartValueDampf = 1000.0; 
const unsigned int defaultTuningLookBackDampf = 30;

PID_ATune* autoTuneWasser;
PID_ATune* autoTuneDampf;
bool autoTuneWasserActive = false;
bool autoTuneDampfActive  = false;

// Boost-Feature
bool boostWasserActive              = false; 
const bool defaultBoostWasserActive = false;
bool boostDampfActive               = false; 
const bool defaultBoostDampfActive  = false;

// Heizen oberhalb Setpoint verhindern
bool preventHeatAboveSetpointWasser = false; // Standardmäßig AUS
const bool defaultPreventHeatAboveSetpointWasser = false;
bool preventHeatAboveSetpointDampf = false;  // Standardmäßig AUS
const bool defaultPreventHeatAboveSetpointDampf = false;

/************************************************************************************
 * Shot-Timer Variablen
 ************************************************************************************/

unsigned long shotStartTime           = 0;
unsigned long shotEndTime             = 0;
bool shotActive                       = false;
unsigned long lastShotDurationMillis  = 0; // Speichert die Dauer des letzten Shots in ms

/************************************************************************************
 * Shot-Zähler und Betriebszeit
 ************************************************************************************/

unsigned long shotCounter           = 0;    // Shots > 20 Sekunden
unsigned long totalRuntime          = 0;    // Gesamt-Betriebszeit in Sekunden
unsigned long lastRuntimeSave       = 0;    // Wann zuletzt gespeichert
const unsigned long initialRuntimeSaveDelay = 10 * 60 * 1000UL; // 10 Minuten
const unsigned long subsequentRuntimeSaveInterval = 5 * 60 * 1000UL; // 5 Minuten
bool initialRuntimeSaveDone = false;

/************************************************************************************
 * Wartungserinnerung Variablen
 ************************************************************************************/
int maintenanceInterval = 0; // Intervall in Shots (0 = deaktiviert)
unsigned long maintenanceIntervalCounter = 0; // Zähler für Wartungsintervall
bool displayMaintenanceMessage = false; // Flag, ob Meldung aktiv angezeigt wird
unsigned long maintenanceMessageStartTime = 0; // Startzeit der Meldungsanzeige

/************************************************************************************
 * Verzögerungen für Anzeigen
 ************************************************************************************/

int delayInit1 = 1500;  // Display-Bild am Anfang
int delayInit2 = 3000;  // Warte für Anzeige
unsigned long previousMillis = 0;        // für Intervall z.B. 250ms
const unsigned long interval = 250;      // PID-Berechnungsintervall

/************************************************************************************
 * LittleFS
 ************************************************************************************/
 File fsUploadFile; // Globale Variable für die Datei während des Uploads

/************************************************************************************
 * Hostname Variable
 ************************************************************************************/
char hostname[33] = "Dual-PID"; // Max 32 Zeichen + Nullterminator

/************************************************************************************
 * WiFi Signal Bitmap (Display-Anzeige)
 ************************************************************************************/

static const unsigned char PROGMEM wifiSymbol[] = {
  0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
  0x27, 0x20, 0x08, 0x80, 0x02, 0x00
};

/************************************************************************************
 * Gemeinsame CSS-Styles (PROGMEM) - Unverändert
 ************************************************************************************/

static const char commonStyle[] PROGMEM = R"rawliteral(
<style>
  /* Farben und Grundvariablen */
  :root {
    --primary-bg: #000000;     /* Schwarz */
    --secondary-bg: #1a1a1a;   /* Dunkles Grau/Schwarz */
    --accent-color: #d89904;   /* Gold-/Kupferton */
    --text-color: #ffffff;     /* Weiß für Schrift */
    --card-bg-opacity: 0.15;   /* Opazität für Form-Karten-Hintergrund */
  }

  /* Seitenhintergrund: Farbverlauf */
  body {
    margin: 0;
    padding: 0;
    font-family: "Segoe UI", Tahoma, Arial, sans-serif;
    background: linear-gradient(120deg, #000000 0%, #1a1a1a 100%);
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
    min-height: 100vh;
    color: #ffffff;
  }

  /************************************************************************
   * Links / Anker-Tags
   ************************************************************************/
  a,
  a:visited {
    text-decoration: none;
    transition: color 0.2s ease, background-color 0.2s ease;
  }
  a:hover,
  a:focus {
    text-decoration: none;
  }

  .info-link,
  .info-link:visited {
      color: #d89904; /* Oder var(--accent-color), falls das die Akzentfarbe ist */
      text-decoration: none; /* Unterstreichung entfernen (wie schon bei 'a') */
      /* !important ist hier wahrscheinlich nicht mehr nötig, da die Klasse spezifischer ist als 'a' */
  }

  .info-link:hover {
      /* z.B. Helligkeit ändern oder doch unterstreichen */
      filter: brightness(1.2);
      /* text-decoration: underline; */
  }

  /************************************************************************
   * Navigation (Sticky Top Bar)
   ************************************************************************/
  nav {
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(6px);
    padding: 10px 0;
    text-align: center;
    position: sticky;
    top: 0;
    /* <b style='color: #FFCC00;'>WICHTIG:</b>: z-index erhöhen, damit das X-Icon klickbar bleibt */
    z-index: 1000;
    position: relative; /* Basis für absolute Positionierung von Kindern */
  }

  /* Wrapper für die Navigationslinks */
  .nav-links {
    display: inline;
  }

  /* Desktop Navigationslinks */
  .nav-links a {
    color: #fff;
    font-weight: 500;
    text-decoration: none;
    margin: 0 10px;
    padding: 10px 20px;
    border-radius: 8px;
    transition: background 0.3s ease, color 0.3s ease;
    display: inline-block;
  }

  /* Desktop Hover/Focus/Active States */
  .nav-links a:hover,
  .nav-links a:focus,
  .nav-links a:active,
  .nav-links a.active {
    background: var(--accent-color);
    color: #000000;
  }

  /* Hamburger-Icon / Close-Icon Styling */
  .hamburger {
    display: none;
    font-size: 30px; /* Etwas größer für bessere Klickbarkeit */
    font-weight: bold; /* Macht das 'X' oft etwas klarer */
    line-height: 1;
    position: absolute;
    /* Anpassung - weiter nach unten verschoben */
    top: calc(50% + 25px); /* Passe '25px' nach Bedarf an */
    transform: translateY(-50%);
    right: 20px;
    color: var(--text-color);
    cursor: pointer;
    padding: 5px; /* Etwas Padding für größere Klickfläche */
    z-index: 1001; /* Sicherstellen, dass der Button über dem Overlay liegt */
  }
  .hamburger:hover {
    color: #ddd;
  }


  /************************************************************************
   * Formulare (Card-Design, Glas-Effekt)
   ************************************************************************/
  form {
    background: rgba(255, 255, 255, var(--card-bg-opacity));
    backdrop-filter: blur(8px);
    border-radius: 12px;
    padding: 20px;
    margin: 20px auto;
    width: 90%;
    max-width: 600px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    color: var(--text-color);
  }

  /* Überschriften */
  h1 {
    text-align: center;
    margin-top: 20px;
    font-size: 2rem;
  }
  h3 {
    margin-top: 0;
    font-weight: 600;
    padding-bottom: 3px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.3);
  }

  /* Labels */
  label {
    display: block;
    margin: 10px 0 5px;
    font-weight: 600;
  }

  /************************************************************************
   * Eingabefelder (Inputs, Select, Textarea) MIT FOKUS-EFFEKT
   ************************************************************************/
  input[type="text"],
  input[type="password"],
  select,
  textarea {
    width: auto;
    min-width: 200px;
    max-width: 300px;
    display: block;
    margin-bottom: 5px;
    background: rgba(0, 0, 0, 0.4);
    color: var(--text-color);
    border: 1px solid #555; /* Standard-Rand */
    border-radius: 8px;
    padding: 10px;
    box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2);
    transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
  }

  /* Fokus-Effekt FÜR ALLE INPUTS/SELECT/TEXTAREA */
  input[type="text"]:focus,
  input[type="password"]:focus,
  select:focus,
  textarea:focus {
    outline: none;
    background: rgba(0, 0, 0, 0.6);
    /* Orangener Rand und Schein bei Fokus */
    border-color: var(--accent-color, #d89904);
    box-shadow: 0 0 8px rgba(216, 153, 4, 0.3);
  }

  /************************************************************************
   * Buttons
   ************************************************************************/
  input[type="submit"],
  button {
    background: var(--accent-color);
    color: #000000;
    border: none;
    border-radius: 30px;
    padding: 10px 20px;
    margin-top: 12px;
    width: auto;
    min-width: 100px;
    max-width: 200px;
    cursor: pointer;
    font-weight: 600;
    box-shadow: 0 4px 12px rgba(216, 153, 4, 0.3);
    transition: all 0.3s ease;
  }
  input[type="submit"]:hover,
  button:hover {
    opacity: 0.85;
    transform: translateY(-2px);
  }

  /* Style für Danger-Button (Werkseinstellungen) */
  button.danger {
      background: #dc3545; /* Roter Hintergrund */
      color: #ffffff; /* Weiße Schrift */
      box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); /* Roter Schatten */
  }
  button.danger:hover {
      background: #c82333; /* Dunkleres Rot bei Hover */
      opacity: 1; /* Opazität zurücksetzen, da Hintergrund dunkler wird */
      transform: translateY(-2px);
  }

  /************************************************************************
   * Fehlermeldungen / Sensorfehler
   ************************************************************************/
  .sensor-error-message {
    color: #FFCC00; /* Auffälliges Gelb/Gold für Warnungen im Dark Mode */
    text-align: center;
    margin-top: 5px;  /* Weniger Abstand nach oben */
    margin-bottom: 15px; /* Abstand zum Chart-Container */
    padding: 8px 10px;
    background-color: rgba(255, 204, 0, 0.1); /* Sehr dezenter gelber Hintergrund */
    border: 1px solid rgba(255, 204, 0, 0.2); /* Sehr dezenter gelber Rand */
    border-radius: 8px;
    font-weight: 500; /* Etwas fetter als normal */
    font-size: 0.9em;
    max-width: 600px; /* Verhindert zu breite Box auf großen Screens */
    margin-left: auto; /* Zentriert die Box selbst */
    margin-right: auto;
  }


  /************************************************************************
   * Responsive Design - MOBILE OVERLAY HIER IMPLEMENTIERT
   ************************************************************************/
  @media (max-width: 768px) {
    .hamburger {
      display: block; /* Hamburger anzeigen */
    }

    /* Der Link-Wrapper wird zum absolut positionierten Overlay */
    .nav-links {
      display: none;
      position: absolute;
      top: 100%; /* Unter der Nav-Leiste */
      left: 0;
      width: 100%;
      background: rgba(26, 26, 26, 0.95);
      backdrop-filter: blur(8px);
      padding: 10px 0;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
      border-top: 1px solid rgba(255, 255, 255, 0.1);
      /* <b style='color: #FFCC00;'>WICHTIG:</b>: z-index niedriger als der Hamburger/Close-Button */
      z-index: 1000;
    }

    /* Wenn Menü aktiv, Wrapper sichtbar */
    nav.active .nav-links {
      display: block;
    }

    /* Styling der Links im mobilen Overlay */
    nav .nav-links a {
      display: block;
      margin: 0;
      padding: 15px 25px;
      text-align: center; /* Links zentrieren */
      border-radius: 0;
      color: #eee;
      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    }
    nav .nav-links a:last-child {
        border-bottom: none;
    }

    /* Hover/Active für mobile Links */
    nav .nav-links a:hover,
    nav .nav-links a:active,
    nav .nav-links a.active {
      background: var(--accent-color);
      color: #000000;
    }

  } /* Ende @media (max-width: 768px) */


  @media (max-width: 480px) {
    h1 {
      font-size: 1.4rem;
    }
    /* Buttons auf 100% Breite */
    input[type="submit"],
    button {
      width: 100%;
      max-width: none;
    }
    /* Eingabefelder evtl. auch anpassen */
    input[type="text"],
    input[type="password"],
    select,
    textarea {
        max-width: 90%;
        min-width: 150px;
    }
  } /* Ende @media (max-width: 480px) */

</style>
<script>
// Angepasste JavaScript-Funktion für Icon-Wechsel
function toggleMenu() {
  var nav = document.querySelector('nav');
  var hamburger = document.querySelector('.hamburger');
  nav.classList.toggle('active');

  // Prüfen, ob das Menü JETZT aktiv ist und Icon entsprechend ändern
  if (nav.classList.contains('active')) {
    hamburger.innerHTML = '&times;'; // Ändert zu einem 'X' (Schließen-Symbol)
  } else {
    hamburger.innerHTML = '&#9776;'; // Ändert zurück zum Hamburger-Symbol
  }
}

// OPTIONAL: Schließen des Menüs bei Klick außerhalb (Verbessert Usability)
document.addEventListener('click', function(event) {
  var nav = document.querySelector('nav');
  var hamburger = document.querySelector('.hamburger');
  var navLinks = document.querySelector('.nav-links');

  // Prüfen ob Menü überhaupt offen ist
  if (nav.classList.contains('active')) {
    // Prüfen ob Klick außerhalb des Menüs UND außerhalb des Buttons war
    var isClickInsideNavLinks = navLinks.contains(event.target);
    var isClickOnHamburger = hamburger.contains(event.target);

    if (!isClickInsideNavLinks && !isClickOnHamburger) {
      toggleMenu(); // Schließe das Menü
    }
  }
});

// Bestätigungsdialog für Werkseinstellungen
function confirmResetDefaults() {
    return confirm("Sicher, dass Sie alle Einstellungen auf die Werkseinstellungen geladen werden sollen?");
}

</script>
)rawliteral";


/************************************************************************************
 * Gemeinsame Navigation (PROGMEM) - URLs angepasst
 ************************************************************************************/

static const char commonNav[] PROGMEM = R"rawliteral(
  <nav>
    <span class="hamburger" onclick="toggleMenu()">&#9776;</span>
    <div class="nav-links"> <a href="/">PID-Einstellung</a>
      <a href="/PID-Tuning">PID-Tuning</a>
      <a href="/Profile">Profile</a>
      <a href="/Chart">Temperaturverlauf</a>
      <a href="/Fast-Heat-Up">Fast-Heat-Up</a>
      <a href="/ECO">Eco</a>
      <a href="/Info">Info</a>
      <a href="/Netzwerk">WiFi</a>
      <a href="/Firmware">Firmware</a>
    </div>
    </span>
  </nav>
)rawliteral";

/************************************************************************************
 * WiFi-Konfigurationsstrukturen und Funktionen - Hostname hinzugefügt
 ************************************************************************************/

struct WiFiConfig
{
  char ssid[32]        = "";
  char password[64]    = "";
  bool useStaticIP     = false;
  IPAddress staticIP   = IPAddress(192, 168, 4, 1);
  IPAddress gateway    = IPAddress(192, 168, 4, 1);
  IPAddress subnet     = IPAddress(255, 255, 255, 0);
  IPAddress dns        = IPAddress(8, 8, 8, 8);
  // Hostname in WiFi-Config (obwohl global gespeichert, hier für Vollständigkeit)
  // Wird nicht direkt hier gespeichert, sondern über globale Variable `hostname`
};

// Liest die WLAN-Konfiguration aus dem EEPROM
// Gibt true zurück, wenn eine gültige Konfiguration (SSID) geladen wurde
bool loadWiFiConfig(WiFiConfig& config)
{
  char magic[5] = { 0 };
  EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);

  // Lade Hostname (immer, auch wenn keine WiFi-Config da ist)
  EEPROM.get(EEPROM_ADDR_HOSTNAME, hostname);
  hostname[sizeof(hostname)-1] = '\0'; // Nullterminierung sicherstellen
  // Wenn Hostname leer ist (z.B. nach Reset), Default setzen
  if (strlen(hostname) == 0) {
      strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1);
      hostname[sizeof(hostname) - 1] = '\0';
      EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname); // Default im EEPROM speichern
      EEPROM.commit();
  }


  // Falls MagicValue nicht übereinstimmt, Standardwerte setzen
  if (strncmp(magic, storageMagicValue, 4) != 0)
  {
    strcpy(config.ssid, "");
    strcpy(config.password, "");
    config.useStaticIP = false;
    config.staticIP = IPAddress(192, 168, 4, 1);
    config.gateway  = IPAddress(192, 168, 4, 1);
    config.subnet   = IPAddress(255, 255, 255, 0);
    config.dns      = IPAddress(8, 8, 8, 8);
    return false; // Keine gültige Konfiguration gefunden
  }

  // Laden der gespeicherten Daten
  EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
  // Nullterminierung sicherstellen (wichtig!)
  config.ssid[sizeof(config.ssid)-1] = '\0';
  config.password[sizeof(config.password)-1] = '\0';

  // Prüfen, ob SSID leer ist (kann nach AP-Modus-Erzwingung passieren)
  return (strlen(config.ssid) > 0); // True, wenn SSID nicht leer ist
}

// Schreibt die WLAN-Konfiguration ins EEPROM
void saveWiFiConfig(const WiFiConfig& config)
{
  EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue);
  EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
  // Hostname wird separat gespeichert (in handleSaveWiFiConfig)
  EEPROM.commit();
}

// Aktiviert den Access Point Modus und zeigt IP an
void startAPMode()
{
  WiFi.hostname(hostname); 
  WiFi.softAP("Dual-PID", "QuickMill"); // Setzt SSID "Dual-PID" und Passwort "QuickMill"
  Serial.print(F("AP-Mode gestartet. SSID: Dual-PID, Passwort: QuickMill, IP: "));
  Serial.println(WiFi.softAPIP());
}

/************************************************************************************
 * Webserver-Handler: WLAN-Konfigurations-Seite - Hostname hinzugefügt
 ************************************************************************************/

// === PROGMEM Teile definieren (angepasst für Signalstärke & Hostname) ===
static const char wifiPageChunk1[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><title>WLAN-Konfiguration</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char wifiPageChunk2[] PROGMEM = R"rawliteral(</head>)rawliteral"; // Head Ende

// Teil 3a: Body bis VOR Signalstärke-Wert
static const char wifiPageChunk3_a[] PROGMEM = R"rawliteral(
<body><h1>WLAN-Konfiguration</h1><form action='/saveWiFiConfig' method='POST'><h3>Netzwerk</h3><div style='margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);'><label style='margin-right: 5px;'>Aktuelle Signalst&auml;rke:</label><span>)rawliteral"; // Endet vor dem Wert

// Teil 3b: NACH Signalstärke-Wert bis VOR Hostname Value
static const char wifiPageChunk3_b[] PROGMEM = R"rawliteral(</span></div><label for='hostname'>Hostname:</label><input type='text' id='hostname' name='hostname' value=')rawliteral"; // Endet mit value='

// Teil 3c: NACH Hostname Value bis VOR SSID Value
static const char wifiPageChunk3_c[] PROGMEM = R"rawliteral('' required><label for='ssid'>SSID:</label><input type='text' id='ssid' name='ssid' value=')rawliteral"; // Beginnt mit ', endet mit value='

// Restliche Teile wie zuvor, nur Nummerierung angepasst
static const char wifiPageChunk4[] PROGMEM = R"rawliteral('' required><label for='password'>Passwort:</label><input type='text' id='password' name='password' value=')rawliteral"; // Beginnt mit ', endet mit value='
static const char wifiPageChunk5_format[] PROGMEM = R"rawliteral(''><h3>IP-Einstellungen</h3><label><input type='radio' name='ipType' value='dhcp' %s> DHCP</label><label><input type='radio' name='ipType' value='static' %s> Statische IP</label><div id='staticFields' style='display:%s'>)rawliteral"; // Beginnt mit '', endet mit display:%s'>
static const char wifiPageChunk6[] PROGMEM = R"rawliteral(<label for='ip'>IP-Adresse:</label><input type='text' id='ip' name='ip' value=')rawliteral"; // Beginnt normal, endet mit value='
static const char wifiPageChunk7[] PROGMEM = R"rawliteral('><label for='gateway'>Gateway:</label><input type='text' id='gateway' name='gateway' value=')rawliteral"; // Beginnt mit ', endet mit value='
static const char wifiPageChunk8[] PROGMEM = R"rawliteral('><label for='subnet'>Subnetzmaske:</label><input type='text' id='subnet' name='subnet' value=')rawliteral"; // Beginnt mit ', endet mit value='
static const char wifiPageChunk9[] PROGMEM = R"rawliteral(''></div><input type='submit' value='Speichern'></form><form action='/forceAPMode' method='POST' style='margin-top: 20px;'><h3>AP-Modus</h3>Verwendung im Access Point-Modus (AP).<br><br>Netzwerk-Name: Dual-PID<br>Passwort: QuickMill<br>IP-Adresse: 192.168.4.1<br><input type='submit' value='AP-Modus verwenden'></form><script>document.querySelectorAll('input[name="ipType"]').forEach(radio => {radio.addEventListener('change', () => {document.getElementById('staticFields').style.display = radio.value === 'static' ? 'block' : 'none';});});</script></body></html>)rawliteral"; // Beginnt mit '', Rest bis Ende

// --- Hilfsfunktion zur Umrechnung von RSSI in Prozent (Approximation) ---
int calculateSignalPercentage(int rssi) {
  int quality = 0;
  if (rssi <= -100) {
    quality = 0;
  } else if (rssi >= -50) { // Guter Empfang
    quality = 100;
  } else {
    // Lineare Skalierung zwischen -100dBm (0%) und -50dBm (100%)
    quality = 2 * (rssi + 100);
  }
  return quality;
}

void handleWiFiConfig() {
    // EEPROM Lese-Logik (inkl. Hostname aus globaler Variable)
    WiFiConfig currentConfig;
    char magic[5] = { 0 };
    EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
    bool configValid = (strncmp(magic, storageMagicValue, 4) == 0);
    if (configValid) {
        EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig);
        currentConfig.ssid[sizeof(currentConfig.ssid)-1] = '\0';
        currentConfig.password[sizeof(currentConfig.password)-1] = '\0';
    } else {
        strcpy(currentConfig.ssid, "");
        strcpy(currentConfig.password, "");
        currentConfig.useStaticIP = false;
        currentConfig.staticIP = IPAddress(192, 168, 4, 1);
        currentConfig.gateway  = IPAddress(192, 168, 4, 1);
        currentConfig.subnet   = IPAddress(255, 255, 255, 0);
    }

    // --- Signalstärke ermitteln ---
    char signalBuffer[50]; // Puffer für den Anzeigetext
    if (WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) {
        long rssi = WiFi.RSSI(); // Signalstärke in dBm holen
        int percentage = calculateSignalPercentage(rssi);
        // Text formatieren (z.B. "-65 dBm (70%)")
        snprintf(signalBuffer, sizeof(signalBuffer), "%d dBm (%d%%)", (int)rssi, percentage);
    } else if (WiFi.getMode() == WIFI_AP) {
        strcpy(signalBuffer, "AP-Modus (keine Messung)");
        Serial.println(F("WiFi Signal: AP Mode"));
    } else {
        strcpy(signalBuffer, "Nicht verbunden");
        Serial.println(F("WiFi Signal: Not Connected"));
    }
    // --- Ende Signalstärke ---

    // Temporäre Puffer für Senden
    char formatBuffer[256]; // Für snprintf_P
    char ipBuffer[16];

    // ---- Beginn des Sendens ----
    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    server.send(200, F("text/html"), "");

    // Chunk 1: HTML-Start
    server.sendContent_P(wifiPageChunk1);
    // Chunk 1.5: Common Style (global)
    server.sendContent_P(commonStyle);
    // Chunk 2: Head Ende
    server.sendContent_P(wifiPageChunk2);
    // Chunk 2.5: Common Nav (global, URLs angepasst)
    server.sendContent_P(commonNav);
    // Chunk 3a: Body bis VOR Signalwert
    server.sendContent_P(wifiPageChunk3_a); // Endet mit <span>
    // Chunk 3.1: Signalwert senden
    server.sendContent(signalBuffer);       // Sendet den formatierten Signal-String
    // Chunk 3b: NACH Signalwert bis VOR Hostname Value
    server.sendContent_P(wifiPageChunk3_b); // Endet mit value='
    // Chunk 3.2: Hostname senden
    server.sendContent(hostname);           // Sendet den aktuellen Hostnamen
    // Chunk 3c: NACH Hostname bis VOR SSID Value
    server.sendContent_P(wifiPageChunk3_c); // Beginnt mit ', endet mit value='

    // Chunk 4: SSID einfügen (Handle Empty)
    if (strlen(currentConfig.ssid) == 0) {
        server.sendContent(F("SSID eingeben"));
    } else {
        server.sendContent(currentConfig.ssid);
    }
    // Chunk 5: Nach SSID bis vor Passwort
    server.sendContent_P(wifiPageChunk4); // Beginnt mit ', endet mit value='
    // Chunk 6: Passwort einfügen (Handle Empty)
    if (!demoModus) {
        if (strlen(currentConfig.password) == 0) {
             server.sendContent(F("Passwort eingeben"));
        } else {
            server.sendContent(currentConfig.password);
        }
    } else {
        server.sendContent(F("*** *** *** ***"));
    }

    // Chunk 7: Radiobuttons und Div-Start (formatiert)
    snprintf_P(formatBuffer, sizeof(formatBuffer), wifiPageChunk5_format,
               currentConfig.useStaticIP ? "" : "checked",
               currentConfig.useStaticIP ? "checked" : "",
               currentConfig.useStaticIP ? "block" : "none"
              );
    server.sendContent(formatBuffer);

    // === IP Felder (wie zuvor) ===
    server.sendContent_P(wifiPageChunk6);
    snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.staticIP[0], currentConfig.staticIP[1], currentConfig.staticIP[2], currentConfig.staticIP[3]);
    server.sendContent(ipBuffer);
    server.sendContent_P(wifiPageChunk7);
    snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.gateway[0], currentConfig.gateway[1], currentConfig.gateway[2], currentConfig.gateway[3]);
    server.sendContent(ipBuffer);
    server.sendContent_P(wifiPageChunk8);
    snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.subnet[0], currentConfig.subnet[1], currentConfig.subnet[2], currentConfig.subnet[3]);
    server.sendContent(ipBuffer);

    // Chunk 9: Rest der Seite
    server.sendContent_P(wifiPageChunk9);

    // Letzter Chunk zum Abschließen
    server.sendContent(F(""));
    // ---- Ende des Sendens ----
}

/************************************************************************************
 * Handler zum Speichern der WLAN-Konfiguration - Heap-Optimiert (Bestätigungsseite)
 ************************************************************************************/

// Die PROGMEM-Definitionen für die Bestätigungsseite bleiben bestehen
static const char saveConfigHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Einstellungen gespeichert</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";

static const char saveConfigHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

static const char saveConfigHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Einstellungen gespeichert</h1>
<form><p>Die Einstellungen wurden gespeichert. Neustart ...</p></form>
</body>
</html>
)rawliteral";

/************************************************************************************
 * Handler, um den AP-Modus per Knopfdruck zu erzwingen - Heap-Optimiert
 ************************************************************************************/

// Die PROGMEM-Definitionen für die Bestätigungsseite bleiben bestehen
static const char forceAPHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>AP-Modus aktivieren</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";

static const char forceAPHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

static const char forceAPHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>AP-Modus wird aktiviert...</h1>
<form><p>Die Einstellungen wurden gespeichert. Neustart im AP-Modus ...</p></form>
</body>
</html>
)rawliteral";


// --- Überarbeitete Funktion handleForceAPMode ---
void handleForceAPMode()
{
  // --- Logik zum Speichern der leeren Konfig (BLEIBT UNVERÄNDERT) ---
  // Leer-Konfiguration speichern, damit beim nächsten Start AP aktiv wird
  WiFiConfig emptyConfig;
  // Wichtig: Hostname NICHT löschen!
  if(!demoModus) { saveWiFiConfig(emptyConfig); }

  // --- Senden der Bestätigungsseite (Heap-Optimiert) ---
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), ""); // Header senden

  server.sendContent_P(forceAPHtml);     // Chunk 1 (Head Start)
  server.sendContent_P(commonStyle);     // Common CSS
  server.sendContent_P(forceAPHtml2);    // Chunk 2 (Head End)
  server.sendContent_P(commonNav);       // Common Navigation
  server.sendContent_P(forceAPHtml3);    // Chunk 3 (Body)

  server.sendContent(F("")); // Abschluss-Chunk

  // --- Logik nach dem Senden (BLEIBT UNVERÄNDERT) ---
  delay(1000);
  ESP.restart();
}

// --- Überarbeitete Funktion handleSaveWiFiConfig ---
void handleSaveWiFiConfig()
{
  // --- Datenverarbeitung (BLEIBT UNVERÄNDERT) ---
  if (!server.hasArg(F("ssid")))
  {
    server.send(500, F("text/plain"), F("Fehler: SSID nicht gefunden"));
    return;
  }

  WiFiConfig newConfig;
  strncpy(newConfig.ssid, server.arg(F("ssid")).c_str(), sizeof(newConfig.ssid) - 1);
  newConfig.ssid[sizeof(newConfig.ssid) - 1] = '\0'; // Sicherstellen der Nullterminierung

  strncpy(newConfig.password, server.arg(F("password")).c_str(), sizeof(newConfig.password) - 1);
  newConfig.password[sizeof(newConfig.password) - 1] = '\0'; // Sicherstellen der Nullterminierung

  newConfig.useStaticIP = (server.arg(F("ipType")) == "static");

  if (newConfig.useStaticIP)
  {
    newConfig.staticIP.fromString(server.arg(F("ip")));
    newConfig.gateway.fromString(server.arg(F("gateway")));
    newConfig.subnet.fromString(server.arg(F("subnet")));
    // DNS wird hier nicht gespeichert, Standard bleibt
  }

  // Hostname speichern
  if (server.hasArg(F("hostname"))) {
      strncpy(hostname, server.arg(F("hostname")).c_str(), sizeof(hostname) - 1);
      hostname[sizeof(hostname) - 1] = '\0'; // Sicherstellen der Nullterminierung
      // Validierung (optional, z.B. auf erlaubte Zeichen prüfen)
      if (strlen(hostname) == 0) { // Wenn leer, Default verwenden
          strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1);
          hostname[sizeof(hostname) - 1] = '\0';
      }
      EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname); // Hostname ins EEPROM schreiben
  }


  // Im Demo-Modus wird nichts gespeichert, sonst schon
  if(!demoModus) {
      saveWiFiConfig(newConfig); // Speichert SSID, PW, IP-Config
      // EEPROM.commit() wird in saveWiFiConfig gemacht (inkl. Hostname-Speicherung)
  }

  // --- Senden der Bestätigungsseite (Heap-Optimiert) ---
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), ""); // Header senden

  server.sendContent_P(saveConfigHtml);  // Chunk 1 (Head Start)
  server.sendContent_P(commonStyle);     // Common CSS
  server.sendContent_P(saveConfigHtml2); // Chunk 2 (Head End)
  server.sendContent_P(commonNav);       // Common Navigation
  server.sendContent_P(saveConfigHtml3); // Chunk 3 (Body)

  server.sendContent(F("")); // Abschluss-Chunk

  // --- Logik nach dem Senden (BLEIBT UNVERÄNDERT) ---
  delay(5000);
  ESP.restart();
}



// Prüfung der WiFi-Verbindung (Reconnect falls getrennt) - Unverändert
void checkWifiConnection()
{
  if (WiFi.status() != WL_CONNECTED)
  {
    WiFi.reconnect();
    if (WiFi.waitForConnectResult() != WL_CONNECTED)
    {
      Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!"));
    }
  }
}

// WiFi-Symbol aufs Display zeichnen - Unverändert
void drawwifiSymbol(int16_t x, int16_t y)
{
  display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
}

/************************************************************************************
 * Handler zum Rücksetzen der Betriebszeit - URL angepasst
 ************************************************************************************/

void handleResetRuntime()
{
  totalRuntime = 0;
  EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
  EEPROM.commit();
  server.sendHeader(F("Location"), F("/Info")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Handler zum Rücksetzen des Shotzählers - URL angepasst
 ************************************************************************************/

void handleResetShots()
{
  shotCounter = 0;
  EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
  EEPROM.commit();
  server.sendHeader(F("Location"), F("/Info")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Handler zum Rücksetzen des Wartungszählers (über Shot-Zähler)
 ************************************************************************************/
void handleResetMaintenance()
{
    maintenanceIntervalCounter = 0; // Wartungszähler-Reset auf 0
    EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter);
    EEPROM.commit();
    displayMaintenanceMessage = false; // Meldung ggf. ausblenden
    server.sendHeader(F("Location"), F("/Info")); // URL angepasst
    server.send(303);
}


/************************************************************************************
 * Handler für den Export der EEPROM-Einstellungen - Unverändert
 ************************************************************************************/
void handleExportSettings() {

  // Header für Dateidownload setzen
  server.sendHeader("Content-Disposition", "attachment; filename=\"dual_pid_settings.bin\"");
  server.sendHeader("Content-Type", "application/octet-stream");
  server.setContentLength(EEPROM_SIZE); // Wichtig: Korrekte Größe senden

  // EEPROM-Inhalt byteweise senden
  // Puffergröße klein halten, um Speicher zu sparen
  const size_t bufferSize = 64;
  byte buffer[bufferSize];
  size_t bytesSent = 0;

  // Start des Sendens (Status 200 OK)
  server.send(200, "application/octet-stream", "");

  // EEPROM lesen und in Chunks senden
  for (int i = 0; i < EEPROM_SIZE; i += bufferSize) {
    size_t currentChunkSize = (EEPROM_SIZE - i < bufferSize) ? (EEPROM_SIZE - i) : bufferSize;
    for (size_t j = 0; j < currentChunkSize; j++) {
      buffer[j] = EEPROM.read(i + j);
    }
    server.sendContent((const char*)buffer, currentChunkSize);
    bytesSent += currentChunkSize;
    yield(); // Wichtig für Netzwerkstabilität bei längeren Operationen
  }
  // Kein server.stop() hier, da die Verbindung automatisch geschlossen wird.
}


/************************************************************************************
 * Handler für den Import der EEPROM-Einstellungen (Upload-Verarbeitung) - Unverändert
 ************************************************************************************/
// Globale Variablen bleiben gleich
static int eepromWriteAddress = 0;
static bool importSuccessful = false;
static String importStatusMessage = "";

void handleImportUpload() {
  HTTPUpload& upload = server.upload();

  // Demo-Modus Prüfung (Bleibt bestehen)
  if (demoModus) {
    if (upload.status == UPLOAD_FILE_START) {
      importStatusMessage = F("Import im Demo-Modus nicht erlaubt!");
      importSuccessful = false;
    }
    return;
  }

  switch (upload.status) {
    case UPLOAD_FILE_START:

      eepromWriteAddress = 0; // Zähler zurücksetzen
      importSuccessful = false; // Status zurücksetzen
      importStatusMessage = ""; // Nachricht zurücksetzen
      break; 

    case UPLOAD_FILE_WRITE:
      // Prüfen, ob bereits ein Fehler aufgetreten ist (z.B. durch zu viele Daten im vorherigen Chunk)
      if (importStatusMessage.length() > 0) {
           yield();
           break; // Beende die Verarbeitung für diesen Status
      }

      // Schreibe die empfangenen Bytes, aber schütze vor Überlauf
      for (size_t i = 0; i < upload.currentSize; i++) {
        // Prüfen, ob das Schreiben das EEPROM_SIZE Limit überschreiten würde
        if (eepromWriteAddress < EEPROM_SIZE) {
          EEPROM.write(eepromWriteAddress, upload.buf[i]);
          eepromWriteAddress++;
        } else {
          // Es kommen MEHR Daten als erwartet!
          Serial.printf("FEHLER: Mehr als %d Bytes empfangen! Adresse: %d. Breche ab.\n", EEPROM_SIZE, eepromWriteAddress);
          importStatusMessage = F("Importfehler: Empfangene Datei zu gro&szlig;!"); // '&szlig;' für ß
          // Breche die Verarbeitung dieses Chunks und zukünftiger Chunks ab
          goto end_write_processing; // Springe aus der Schleife und dem Case
        }
      }
      end_write_processing:; // Sprungziel für goto
      yield();
      break;

    case UPLOAD_FILE_END:
      // FINALE GRÖSSENPRÜFUNG und Fehlerprüfung

      // 1. Gab es einen Fehler während des Schreibens (z.B. Datei zu groß)?
      if (importStatusMessage.length() > 0) {
           Serial.println("Import nicht erfolgreich aufgrund vorheriger Fehler (z.B. Datei zu groß).");
           importSuccessful = false; // Sicherstellen, dass Flag false ist
      }
      // 2. Wurden exakt EEPROM_SIZE Bytes verarbeitet?
      else if (eepromWriteAddress == EEPROM_SIZE) {
         if (EEPROM.commit()) {
           importSuccessful = true;
           importStatusMessage = F("Einstellungen erfolgreich importiert! Neustart wird durchgef&uuml;hrt..."); // '&uuml;' für ü
         } else {
           Serial.println("FEHLER: EEPROM Commit fehlgeschlagen!");
           importSuccessful = false;
           importStatusMessage = F("Import fehlgeschlagen: EEPROM Commit Fehler!");
         }
      }
      // 3. Wurden weniger oder mehr Bytes verarbeitet (und kein Fehler vorher gesetzt)?
      else {
         Serial.printf("FEHLER: Upload beendet, aber falsche Anzahl Bytes verarbeitet! Verarbeitet: %d, Erwartet: %d\n", eepromWriteAddress, EEPROM_SIZE);
         importSuccessful = false;
         importStatusMessage = F("Import fehlgeschlagen: Falsche Dateigr&ouml;&szlig;e!"); // '&ouml;', '&szlig;'
      }

      // Bereinigung für den nächsten Upload
      eepromWriteAddress = 0;
      break;

    case UPLOAD_FILE_ABORTED:
       Serial.println("EEPROM Import vom Client abgebrochen.");
       importSuccessful = false;
       // Nur Nachricht setzen, wenn nicht schon ein Fehler vorliegt
       if (importStatusMessage.length() == 0) {
           importStatusMessage = F("Import abgebrochen!");
       }
       eepromWriteAddress = 0;
      break;

    default:
       Serial.printf("Unbekannter Upload-Status: %d\n", upload.status);
       break;
  }
}

/************************************************************************************
 * Handler für die Import-Seite (nach dem Upload) - Heap-Optimiert
 ************************************************************************************/

// PROGMEM Chunks für die Seite
static const char importDoneHtmlStart[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><title>Einstellungen Import</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char importDoneHtmlHeadEnd[] PROGMEM = R"rawliteral(</head>)rawliteral";
static const char importDoneHtmlBodyStart[] PROGMEM = R"rawliteral(
<body><h1>Einstellungen Import</h1>
<form><p>)rawliteral"; // Endet vor {status_message}
static const char importDoneHtmlBodyEnd[] PROGMEM = R"rawliteral(</p></form>
</body></html>
)rawliteral"; // Beginnt nach {status_message}

// Überarbeitete Funktion
void handleImportSettings() {
  // Statusvariablen importSuccessful und importStatusMessage werden von handleImportUpload gesetzt

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  server.sendContent_P(importDoneHtmlStart);
  server.sendContent_P(commonStyle);
  server.sendContent_P(importDoneHtmlHeadEnd);
  server.sendContent_P(commonNav);
  server.sendContent_P(importDoneHtmlBodyStart);

  // Statusmeldung senden (importStatusMessage ist globale String-Variable)
  if (importStatusMessage.length() > 0) {
      server.sendContent(importStatusMessage);
  }

  server.sendContent_P(importDoneHtmlBodyEnd);
  server.sendContent(F("")); // Abschluss-Chunk

  // Neustart-Logik bleibt unverändert
  if (importSuccessful) {
    Serial.println("Neustart in 5 Sekunden nach EEPROM-Import...");
    delay(5000);
    ESP.restart();
  }
}

/************************************************************************************
 * Funktion zum Zurücksetzen auf Werkseinstellungen
 ************************************************************************************/
void resetToDefaults() {
    Serial.println("Zurücksetzen auf Werkseinstellungen...");

    // PID & Offset
    EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, defaultSetpointDampf);
    EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, defaultSetpointWasser);
    EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, defaultOffsetDampf);
    EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, defaultOffsetWasser);
    EEPROM.put(EEPROM_ADDR_KP_DAMPF, defaultKpDampf);
    EEPROM.put(EEPROM_ADDR_KI_DAMPF, defaultKiDampf);
    EEPROM.put(EEPROM_ADDR_KD_DAMPF, defaultKdDampf);
    EEPROM.put(EEPROM_ADDR_KP_WASSER, defaultKpWasser);
    EEPROM.put(EEPROM_ADDR_KI_WASSER, defaultKiWasser);
    EEPROM.put(EEPROM_ADDR_KD_WASSER, defaultKdWasser);
    EEPROM.put(EEPROM_ADDR_WINDOWSIZE_WASSER, defaultWindowSizeWasser);
    EEPROM.put(EEPROM_ADDR_WINDOWSIZE_DAMPF, defaultWindowSizeDampf);

    // Boost-Funktion
    EEPROM.put(EEPROM_ADDR_BOOST_WASSER_ACTIVE, defaultBoostWasserActive);
    EEPROM.put(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, defaultBoostDampfActive);

    // Heiz-Begrenzung
    EEPROM.put(EEPROM_ADDR_PREVENTHEAT_WASSER, defaultPreventHeatAboveSetpointWasser);
    EEPROM.put(EEPROM_ADDR_PREVENTHEAT_DAMPF, defaultPreventHeatAboveSetpointDampf);

    // Eco Modus
    EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, defaultEcoModeMinutes);
    EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, defaultEcoModeTempWasser);
    EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, defaultEcoModeTempDampf);
    EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, false); // Default für Dynamic Eco
    EEPROM.put(EEPROM_ADDR_STEAM_DELAY, defaultDampfVerzoegerung);

    // Geräteinfo
    char tempInfo[50];
    strncpy(tempInfo, defaultInfoHersteller.c_str(), sizeof(tempInfo) - 1);
    tempInfo[sizeof(tempInfo) - 1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, tempInfo);
    strncpy(tempInfo, defaultInfoModell.c_str(), sizeof(tempInfo) - 1);
    tempInfo[sizeof(tempInfo) - 1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_MODELL, tempInfo);
    strncpy(tempInfo, defaultInfoZusatz.c_str(), sizeof(tempInfo) - 1);
    tempInfo[sizeof(tempInfo) - 1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, tempInfo);

    // Fast Heat Up
    EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, false); // Default für Fast Heat Up

    // Profile (mit Default-Werten überschreiben)
    TemperatureProfile defaultProfile;
    defaultProfile.setpointWasser = defaultSetpointWasser;
    defaultProfile.setpointDampf  = defaultSetpointDampf;
    defaultProfile.ecoModeMinutes = defaultEcoModeMinutes;
    defaultProfile.dynamicEcoActive = false;
    defaultProfile.dampfVerzoegerung = defaultDampfVerzoegerung;
    strncpy(defaultProfile.profileName, "Unbenannt", sizeof(defaultProfile.profileName));
    defaultProfile.profileName[sizeof(defaultProfile.profileName) - 1] = '\0';
    EEPROM.put(EEPROM_ADDR_PROFILE_1, defaultProfile);
    EEPROM.put(EEPROM_ADDR_PROFILE_2, defaultProfile);

    // AutoTune Parameter
    EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, defaultTuningStepWasser);
    EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, defaultTuningNoiseWasser);
    EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, defaultTuningStartValueWasser);
    EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, defaultTuningLookBackWasser);
    EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, defaultTuningStepDampf);
    EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, defaultTuningNoiseDampf);
    EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, defaultTuningStartValueDampf);
    EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, defaultTuningLookBackDampf);

    // Sicherheitslimits (verwenden die globalen Variablen, die beim Start initialisiert wurden)
    EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); // Diese sollten eigentlich auch Defaults haben
    EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf);   // TODO: Eigene Defaults definieren?

    // // WiFi Konfiguration löschen (erzwingt AP-Modus beim nächsten Start)
    // char emptyMagic[5] = {0};
    // EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, emptyMagic);

    // Hostname auf Standard
    strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1);
    hostname[sizeof(hostname) - 1] = '\0';
    EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname);

    // Wartungsintervall zurücksetzen
    EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, 0UL);
    EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL, defaultMaintenanceInterval);

    // // Zähler zurücksetzen (optional, aber sinnvoll)
    // EEPROM.put(EEPROM_ADDR_RUNTIME, 0UL);
    // EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, 0UL);

    // Magic Value für allgemeine Einstellungen setzen (wichtig!)
    EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue);

    // Alle Änderungen ins EEPROM schreiben
    if (EEPROM.commit()) {
    } else {
        Serial.println("FEHLER: Schreiben der Werkseinstellungen ins EEPROM fehlgeschlagen!");
    }

    // WICHTIG: Globale Variablen auch auf Default setzen, damit der laufende Betrieb korrekt ist
    //          bis zum Neustart (obwohl ein Neustart nach Reset empfohlen wird)
    maintenanceInterval = defaultMaintenanceInterval;
    maintenanceIntervalCounter = 0;
    boostWasserActive = defaultBoostWasserActive;
    boostDampfActive = defaultBoostDampfActive;
    preventHeatAboveSetpointWasser = defaultPreventHeatAboveSetpointWasser;
    preventHeatAboveSetpointDampf = defaultPreventHeatAboveSetpointDampf;
    strncpy(hostname, defaultHostname.c_str(), sizeof(hostname)-1); hostname[sizeof(hostname)-1] = '\0';

    windowSizeWasser = defaultWindowSizeWasser;
    windowSizeDampf = defaultWindowSizeDampf;

    pidWasser.SetOutputLimits(0, windowSizeWasser);
    pidDampf.SetOutputLimits(0, windowSizeDampf);
}

/************************************************************************************
 * Handler für das Zurücksetzen auf Werkseinstellungen - Heap-Optimiert
 ************************************************************************************/

// PROGMEM Chunks für die Seite
static const char resetDoneHtmlStart[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><title>Werkseinstellungen</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char resetDoneHtmlHeadEnd[] PROGMEM = R"rawliteral(</head>)rawliteral";
static const char resetDoneHtmlBody[] PROGMEM = R"rawliteral(
<body><h1>Werkseinstellungen</h1>
<form><p>Alle Einstellungen wurden auf die Werkseinstellungen zur&uuml;ckgesetzt. Das Ger&auml;t wird neu gestartet...</p></form>
</body></html>
)rawliteral";

// Überarbeitete Funktion
void handleResetDefaults() {
    // Logik zum Resetten bleibt unverändert
    if (!demoModus) {
        resetToDefaults(); // Führt das eigentliche Reset durch
    } else {
        server.send(403, F("text/plain"), F("Reset im Demo-Modus nicht erlaubt."));
        return;
    }

    // Bestätigungsseite senden (Heap-optimiert)
    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    server.send(200, F("text/html"), "");

    server.sendContent_P(resetDoneHtmlStart);
    server.sendContent_P(commonStyle);
    server.sendContent_P(resetDoneHtmlHeadEnd);
    server.sendContent_P(commonNav);
    server.sendContent_P(resetDoneHtmlBody); // Body ist komplett statisch
    server.sendContent(F("")); // Abschluss-Chunk

    // Neustart-Logik bleibt unverändert
    delay(5000);
    ESP.restart();
}

/************************************************************************************
 * Variable für Systemstartzeit (relevant für die Dampf-Verzögerung) - Unverändert
 ************************************************************************************/

unsigned long startupTime;

/************************************************************************************
 * setup(): Initialisierung des Systems - Angepasst für neue EEPROM-Werte, Hostname, URLs
 ************************************************************************************/

void setup()
{
  Serial.begin(115200);
  EEPROM.begin(EEPROM_SIZE); // Größe angepasst

  pinMode(SSR_DAMPF_PIN, OUTPUT);
  pinMode(SSR_WASSER_PIN, OUTPUT);
  pinMode(SHOT_TIMER_PIN, INPUT_PULLUP);

  // Initialisiere SSRs als AUS
  digitalWrite(SSR_DAMPF_PIN, LOW);
  digitalWrite(SSR_WASSER_PIN, LOW);

  // Prüfen, ob MagicValue bereits im EEPROM liegt
  char storedMagicValue[5] = { 0 };
  bool magicValueVorhanden = false;
  EEPROM.get(EEPROM_ADDR_MAGICVALUE, storedMagicValue);

  if (strncmp(storedMagicValue, storageMagicValue, 4) == 0)
  {
    magicValueVorhanden = true;
  }
  else
  {
    magicValueVorhanden = false;
    // Hier noch KEINE Defaults schreiben, das passiert unten gebündelt
    // Nur Magic Value setzen, falls er fehlt
    // EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue);
    // EEPROM.commit(); // Commit erfolgt später
  }

  // Werte aus dem EEPROM laden, wenn vorhanden, sonst mit Default-Werten initialisieren
  if (magicValueVorhanden)
  {
    EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
    EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
    EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
    EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
    EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf);
    EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf);
    EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf);
    EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser);
    EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser);
    EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser);
    EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
    EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
    EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
    EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
    EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell);
    EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
    EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
    EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
    EEPROM.get(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
    // --- Bestehende Tuning-Parameter laden (jetzt Wasser) ---
    EEPROM.get(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser);      // Name geändert
    EEPROM.get(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser);      // Name geändert
    EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser); // Name geändert
    EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser);    // Name geändert
    // --- Dampf-Tuning-Parameter laden ---
    EEPROM.get(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf);
    EEPROM.get(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf);
    EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf);
    EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf);
    // Sicherheitslimits laden
    EEPROM.get(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser);
    EEPROM.get(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf);
    // Wartungsintervall laden
    EEPROM.get(EEPROM_ADDR_MAINTENANCE_INTERVAL, maintenanceInterval);
    // Boost-Funktion
    EEPROM.get(EEPROM_ADDR_BOOST_WASSER_ACTIVE, boostWasserActive);
    EEPROM.get(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, boostDampfActive);
    // Heiz-Begrenzung
    EEPROM.get(EEPROM_ADDR_PREVENTHEAT_WASSER, preventHeatAboveSetpointWasser);
    EEPROM.get(EEPROM_ADDR_PREVENTHEAT_DAMPF, preventHeatAboveSetpointDampf);
    // Zeitfenster laden
    EEPROM.get(EEPROM_ADDR_WINDOWSIZE_WASSER, windowSizeWasser);
    EEPROM.get(EEPROM_ADDR_WINDOWSIZE_DAMPF, windowSizeDampf);

    // Sicherstellen, dass Strings nullterminiert sind
    infoHersteller[sizeof(infoHersteller)-1] = '\0';
    infoModell[sizeof(infoModell)-1] = '\0';
    infoZusatz[sizeof(infoZusatz)-1] = '\0';

  }
  else // Magic Value fehlt -> ALLES auf Default setzen
  {
    // Setze globale Variablen auf Defaults
    SetpointDampf = defaultSetpointDampf;
    SetpointWasser = defaultSetpointWasser;
    OffsetDampf = defaultOffsetDampf;
    OffsetWasser = defaultOffsetWasser;
    KpDampf = defaultKpDampf;
    KiDampf = defaultKiDampf;
    KdDampf = defaultKdDampf;
    KpWasser = defaultKpWasser;
    KiWasser = defaultKiWasser;
    KdWasser = defaultKdWasser;
    ecoModeMinutes = defaultEcoModeMinutes;
    ecoModeTempWasser = defaultEcoModeTempWasser;
    ecoModeTempDampf = defaultEcoModeTempDampf;
    strncpy(infoHersteller, defaultInfoHersteller.c_str(), sizeof(infoHersteller)-1); infoHersteller[sizeof(infoHersteller)-1] = '\0';
    strncpy(infoModell, defaultInfoModell.c_str(), sizeof(infoModell)-1); infoModell[sizeof(infoModell)-1] = '\0';
    strncpy(infoZusatz, defaultInfoZusatz.c_str(), sizeof(infoZusatz)-1); infoZusatz[sizeof(infoZusatz)-1] = '\0';
    dynamicEcoActive = false;
    fastHeatUpAktiv = false;
    dampfVerzoegerung = defaultDampfVerzoegerung;
    tuningStepWasser = defaultTuningStepWasser;
    tuningNoiseWasser = defaultTuningNoiseWasser;
    tuningStartValueWasser = defaultTuningStartValueWasser;
    tuningLookBackWasser = defaultTuningLookBackWasser;
    tuningStepDampf = defaultTuningStepDampf;
    tuningNoiseDampf = defaultTuningNoiseDampf;
    tuningStartValueDampf = defaultTuningStartValueDampf;
    tuningLookBackDampf = defaultTuningLookBackDampf;
    maintenanceInterval = defaultMaintenanceInterval;
    maintenanceIntervalCounter = 0;
    boostWasserActive = defaultBoostWasserActive;
    boostDampfActive = defaultBoostDampfActive;
    preventHeatAboveSetpointWasser = defaultPreventHeatAboveSetpointWasser;
    preventHeatAboveSetpointDampf = defaultPreventHeatAboveSetpointDampf;
    resetToDefaults(); // Schreibt alle Defaults und committet
  }

  // WiFi-Einstellungen laden und ggf. herstellen
  WiFiConfig wifiConfig;
  bool hasWiFiConfig = loadWiFiConfig(wifiConfig); // Lädt auch Hostname

  WiFi.hostname(hostname); // Hostname setzen VOR begin/softAP

  if (hasWiFiConfig)
  {
    WiFi.mode(WIFI_STA);

    if (wifiConfig.useStaticIP)
    {
      WiFi.config(wifiConfig.staticIP, wifiConfig.gateway, wifiConfig.subnet);
    }

    Serial.printf("Verbinde mit SSID: %s, Hostname: %s\n", wifiConfig.ssid, hostname);
    WiFi.begin(wifiConfig.ssid, wifiConfig.password);
    int retries = 0;

    while (WiFi.status() != WL_CONNECTED && retries < 20)
    {
      delay(500);
      Serial.print(F("."));
      retries++;
    }

    if (WiFi.status() == WL_CONNECTED)
    {
      Serial.print(F("IP-Adresse: ")); Serial.println(WiFi.localIP());
      // Demo-Modus aktivieren, wenn es sich beim verwendeten Controller um den Demo-Controller handelt
      if(WiFi.localIP() == demoModusIP) {demoModus = true;}
      // mDNS starten
      if (MDNS.begin(hostname)) {
          Serial.println("mDNS Responder gestartet");
          MDNS.addService("http", "tcp", 80); // Service für Webserver anmelden
      } else {
          Serial.println("Fehler beim Starten des mDNS Responders!");
      }
    }
    else
    {
      Serial.println(F("\nKonnte keine Verbindung herstellen! Starte AP-Modus..."));
      startAPMode();
    }
  }
  else
  {
    Serial.println("Keine WLAN-Konfiguration gefunden. Starte AP-Modus...");
    startAPMode();
  }

  // Wartungszähler, Betriebszeit und Shotcounter aus EEPROM laden
  EEPROM.get(EEPROM_ADDR_RUNTIME, totalRuntime);
  if (totalRuntime == ULONG_MAX || totalRuntime > (86400UL * 365 * 10)) { // Plausibilitätscheck (z.B. > 10 Jahre)
      Serial.println("Ungültige Betriebszeit im EEPROM, setze auf 0.");
      totalRuntime = 0;
      EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Korrigierten Wert speichern
      EEPROM.commit();
  }

  EEPROM.get(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
  if (shotCounter == ULONG_MAX || shotCounter > 1000000UL) // Plausibilitätscheck
  {
    Serial.println("Ungültiger Shotcounter im EEPROM, setze auf 0.");
    shotCounter = 0;
    EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
    EEPROM.commit();
  }

  EEPROM.get(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter);
  if (maintenanceIntervalCounter == ULONG_MAX || maintenanceIntervalCounter > 1000000UL) // Plausibilitätscheck
  {
    Serial.println("Ungültiger Wartungszähler im EEPROM, setze auf 0.");
    maintenanceIntervalCounter = 0;
    EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter);
    EEPROM.commit();
  }

  if (fastHeatUpAktiv)
  {
    fastHeatUpHeating = true;
  }

  // OLED-Initialisierung
  Wire.begin(OLED_SDA, OLED_SCK);
  if (!display.begin(0x3C, true))
  {
    Serial.println(F("Display konnte nicht initialisiert werden!"));
  }

  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.println(infoHersteller);
  display.println(infoModell);
  display.println(infoZusatz);
  display.println("");
  display.println(F("Dual-PID-Controller"));
  display.print(F("Version: "));
  display.println(version);
  display.println(F("von Thomas M\201ller")); // \201 ist ü für manche Displays
  display.display();
  delay(delayInit1);

  // --- PID-Regler Konfiguration ---
  // Plausibilitätsprüfung - Bei Bedarf auf Standard-Wert setzen, wenn nicht plausibel
  if (windowSizeWasser == 0 || windowSizeWasser > 60000) {
        windowSizeWasser = defaultWindowSizeWasser;
    }
  if (windowSizeDampf == 0 || windowSizeDampf > 60000) {
        windowSizeDampf = defaultWindowSizeDampf;
    }
  // Setze Output-Grenzen auf die Fenstergröße für zeitproportionale Steuerung
  pidWasser.SetOutputLimits(0, windowSizeWasser);
  pidDampf.SetOutputLimits(0, windowSizeDampf);

  // Setze PID-Parameter
  pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
  pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);

  // Setze PID-Modus auf AUTOMATIC
  pidDampf.SetMode(AUTOMATIC);
  pidWasser.SetMode(AUTOMATIC);

  // Initialisiere Startzeiten für die Zeitfenster
  windowStartTimeWasser = millis();
  windowStartTimeDampf = millis();

  // Anzeige zur WiFi-Verbindung
  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.println(infoHersteller);
  display.println(infoModell);
  display.println(infoZusatz);
  display.println("");
  if (WiFi.status() != WL_CONNECTED)
  {
    display.println(F("WiFi: AP-Modus"));
    display.print(F("SSID: ")); display.println(hostname);
    display.print(F("IP: ")); display.println(WiFi.softAPIP());
  }
  else
  {
    display.println(F("Webserver gestartet"));
    display.print(F("IP: ")); display.println(WiFi.localIP());
    display.print(F("Host: ")); display.print(hostname); display.println(".local");
  }
  display.display();
  delay(delayInit2);

  // --- LittleFS Initialisierung ---
  if (!LittleFS.begin()) {
    Serial.println("LittleFS Mount fehlgeschlagen!");
    // Hier könntest du entscheiden, ob der ESP anhalten soll oder
    // versuchen soll, das Dateisystem zu formatieren (LittleFS.format())
    // Fürs Erste geben wir nur einen Fehler aus.
    display.clearDisplay();
    display.setCursor(0,0);
    display.println("FEHLER:");
    display.println("LittleFS konnte");
    display.println("nicht initialisiert");
    display.println("werden!");
    display.display();
    while(1) delay(100); // Anhalten bei Fehler
  } else {
    Serial.println("LittleFS erfolgreich gemountet.");
    // Optional: Liste beim Start Dateien auf
    Dir dir = LittleFS.openDir("/");
    Serial.println("Dateien im LittleFS:");
    while (dir.next()) {
      Serial.print("  - ");
      Serial.print(dir.fileName());
      Serial.print(" (");
      Serial.print(dir.fileSize());
      Serial.println(" Bytes)");
    }
    Serial.println("--------------------");
  }

  // Routen für Webserver (URLs angepasst)

  //Hauptseite
  server.on("/", handleRoot);
  server.on("/updateSettings", handleSettingsUpdate);
  //Info
  server.on("/Info", handleInfo);
  server.on("/updateInfoSettings", handleInfoUpdate); 
  // AutoTune
  server.on("/AutoTune-Wasser", handleAutoTuneWasser);
  server.on("/AutoTune-Dampf", handleAutoTuneDampf);
  server.on("/updateAutotuneSettings", HTTP_POST, handleUpdateAutotuneSettings);
  server.on("/PID-Tuning-Abbruch", HTTP_POST, handlePidTuningAbbruch); 
  server.on("/PID-Tuning", handlePidTuning);
  // ECO-Modus
  server.on("/ECO", handleEco);
  server.on("/updateEcoSettings", handleEcoUpdate);
  // Firmware
  server.on("/Firmware", handleFirmware);
  server.on("/update", HTTP_POST, handleFirmwareUpdateResponse, handleFirmwareUploadProgress);
  // FAst-Heat-Up
  server.on("/Fast-Heat-Up", handleFastHeatUp); 
  server.on("/updateFast-Heat-Up-Settings", handleFastHeatUpSettings);
  // Dateimanager
  server.on("/Dateimanager", HTTP_GET, handleFileManager); 
  server.on("/Datei-Upload", HTTP_POST, handleFileUploaded, handleFileUpload);
  server.on("/Datei-Entfernen", HTTP_GET, handleFileDelete);
  server.on("/Datei-Download", HTTP_GET, handleFileDownload);
  //Chart
  server.on("/Chart-Daten", HTTP_GET, handleChartData);
  server.on("/Chart", handleCharts);
  server.on("/chart.umd.min.js", HTTP_GET, handleServeChartJs);
  // Netzwerk / WiFi
  server.on("/Netzwerk", handleWiFiConfig);
  server.on("/saveWiFiConfig", handleSaveWiFiConfig);
  server.on("/forceAPMode", handleForceAPMode);
  // Profile
  server.on("/Profile", handleProfiles);
  server.on("/saveProfile1", handleSaveProfile1);
  server.on("/loadProfile1", handleLoadProfile1);
  server.on("/saveProfile2", handleSaveProfile2);
  server.on("/loadProfile2", handleLoadProfile2);
  // Einstellungen Import & Export + Reset
  server.on("/exportSettings", HTTP_GET, handleExportSettings);
  server.on("/importSettings", HTTP_POST, handleImportSettings, handleImportUpload);
  server.on("/resetDefaults", HTTP_POST, handleResetDefaults);
  // Zähler & Wartung
  server.on("/updateMaintenanceSettings", handleMaintenanceUpdate);
  server.on("/resetRuntime", handleResetRuntime);
  server.on("/resetShots", handleResetShots);
  server.on("/resetMaintenance", handleResetMaintenance);
  
  // Webserver starten
  server.begin();
  Serial.println(F("Webserver gestartet"));
  startupTime = millis();
}

/************************************************************************************
 * loop(): Hauptschleife - mDNS Update hinzugefügt
 ************************************************************************************/

void loop()
{
  unsigned long currentMillis = millis();

  // --- Temperaturmessung und PID-Berechnung (im Intervall) ---
  if (currentMillis - previousMillis >= interval)
  {
    previousMillis = currentMillis;

    // Temperaturmessung
    InputDampf  = round(thermocouple.readCelsius() + OffsetDampf);
    InputWasser = round(readNTCTemperature() + OffsetWasser);

    // --- Angepasste Sicherheitsüberprüfung Wasser ---
    // 1. Echter Sensorfehler? (NaN oder negativ)
    if (InputWasser < 0.0 || isnan(InputWasser))
    {
        wasserSensorError = true;     // Echter Sensorfehler
        wasserSafetyShutdown = false; // Kein Shutdown-Grund, da Wert ungültig
        OutputWasser = 0;             // Heizen stoppen
        if (!wasserSensorError) { // Nur einmal loggen, wenn Fehler neu auftritt
             Serial.println("WARNUNG: Wassertemperatursensor liefert ungültigen Wert!");
        }
    }
    // 2. Sensor OK, aber Sicherheitslimit überschritten?
    else if (InputWasser > maxTempWasser)
    {
        wasserSensorError = false;    // Sensor liefert ja gültigen Wert
        wasserSafetyShutdown = true;  // Sicherheitsabschaltung aktiv!
        OutputWasser = 0;             // Heizen stoppen
        if (!wasserSafetyShutdown) { // Nur einmal loggen, wenn Shutdown neu auftritt
            Serial.printf("WARNUNG: Wassertemperatur (%.1f°C) über Sicherheitslimit (%.1f°C)! Sicherheitsabschaltung.\n", InputWasser, maxTempWasser);
        }
    }
    // 3. Sensor OK und Temperatur OK
    else
    {
        // Beide Fehler/Warn-Flags zurücksetzen, wenn alles in Ordnung ist
        if (wasserSensorError) Serial.println("INFO: Wassertemperatursensor wieder OK.");
        wasserSensorError = false;
        if (wasserSafetyShutdown) Serial.println("INFO: Wassertemperatur wieder im sicheren Bereich.");
        wasserSafetyShutdown = false;

        // --- NEUE BOOST LOGIK (oder normale PID-Berechnung) ---
        // Bedingungen für Boost: Feature aktiv UND nicht im Eco-Modus UND kein AutoTune Wasser aktiv UND Temp < X % vom Sollwert
        bool activateBoost = boostWasserActive
                           && !ecoModeAktiv
                           && !autoTuneWasserActive
                           && (InputWasser < (SetpointWasser * 0.95));

        if (activateBoost)
        {
            OutputWasser = windowSizeWasser; // Boost: Volle Leistung
        }
        else if (!autoTuneWasserActive) // Kein Boost ODER Bedingung nicht erfüllt UND kein AutoTune
        {
            pidWasser.Compute(); // Normaler PID-Betrieb
        }
        // Bei AutoTune wird OutputWasser in handleAutoTune() gesetzt
    } // Ende Wasser Prüfung

    // --- Angepasste Sicherheitsüberprüfung Dampf ---
    // 1. Echter Sensorfehler? (NaN oder negativ)
    if (isnan(InputDampf) || InputDampf < 0.0)
    {
        dampfSensorError = true;    // Echter Sensorfehler
        dampfSafetyShutdown = false;// Kein Shutdown-Grund, da Wert ungültig
        OutputDampf = 0;            // Heizen stoppen
        if (!dampfSensorError) { // Nur einmal loggen
            Serial.println("WARNUNG: Dampftemperatursensor liefert ungültigen Wert!");
        }
    }
    // 2. Sensor OK, aber Sicherheitslimit überschritten?
    else if (InputDampf > maxTempDampf)
    {
        dampfSensorError = false;   // Sensor liefert ja gültigen Wert
        dampfSafetyShutdown = true; // Sicherheitsabschaltung aktiv!
        OutputDampf = 0;            // Heizen stoppen
        if (!dampfSafetyShutdown) { // Nur einmal loggen
             Serial.printf("WARNUNG: Dampftemperatur (%.1f°C) über Sicherheitslimit (%.1f°C)! Sicherheitsabschaltung.\n", InputDampf, maxTempDampf);
        }
    }
    // 3. Sensor OK und Temperatur OK
    else
    {
        // Beide Fehler/Warn-Flags zurücksetzen, wenn alles in Ordnung ist
        if (dampfSensorError) Serial.println("INFO: Dampftemperatursensor wieder OK.");
        dampfSensorError = false;
         if (dampfSafetyShutdown) Serial.println("INFO: Dampftemperatur wieder im sicheren Bereich.");
        dampfSafetyShutdown = false;

        // Prüfen, ob die Startverzögerung für Dampf noch aktiv ist
        bool steamDelayActive = (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL));

        if (steamDelayActive)
        {
            OutputDampf = 0; // Während Verzögerung heizen verhindern
        }
        else // Keine Startverzögerung mehr aktiv
        {
            // --- NEUE DAMPF BOOST LOGIK (oder normale PID-Berechnung) ---
            bool activateBoostDampf = boostDampfActive
                                   && !ecoModeAktiv
                                   && !autoTuneDampfActive
                                   && (InputDampf < (SetpointDampf * 0.75));

            if (activateBoostDampf)
            {
                OutputDampf = windowSizeDampf; // Boost Dampf aktiv
            }
            else if (!autoTuneDampfActive) // Kein Boost ODER Bedingung nicht erfüllt UND kein AutoTune
            {
                pidDampf.Compute(); // Normaler PID-Betrieb für Dampf
            }
            // Bei AutoTune wird OutputDampf in handleAutoTune() gesetzt
        } // Ende "Keine Startverzögerung"
    } // Ende Dampf Prüfung

    // AutoTune-Logik (unverändert)
    handleAutoTune();
  } // Ende des Temperaturmessungs-/PID-Berechnungsintervalls

  // --- Zeitproportionale SSR-Ansteuerung (wird kontinuierlich geprüft) ---
  unsigned long now = millis();

  // --- Wasser SSR ---
  // Heizen wird gestoppt, wenn ein Sensorfehler ODER eine Sicherheitsabschaltung vorliegt
  if (wasserSensorError || wasserSafetyShutdown)
  {
      digitalWrite(SSR_WASSER_PIN, LOW); // Sicherheit: Bei Fehler/Shutdown immer AUS
  }
  else // Nur heizen, wenn KEIN Fehler UND KEIN Shutdown
  {
      // Fenster neu starten, wenn Zeit abgelaufen
      if (now - windowStartTimeWasser > windowSizeWasser)
      {
          windowStartTimeWasser += windowSizeWasser;
      }
      // Prüfen, ob geheizt werden soll/darf ***
      // Bedingung 1: PID/Boost will heizen (innerhalb des Zeitfensters)
      bool pidWantsHeat = (OutputWasser > 0) && (now < (windowStartTimeWasser + (unsigned long)OutputWasser));
      // Bedingung 2: Überhitzungsschutz-Feature aktiv UND Temperatur ist bereits über Sollwert
      bool preventOverheatActive = preventHeatAboveSetpointWasser && (InputWasser > SetpointWasser);

      // Nur heizen, wenn Bedingung 1 wahr ist UND Bedingung 2 falsch ist
      if (pidWantsHeat && !preventOverheatActive)
      {
          digitalWrite(SSR_WASSER_PIN, HIGH);
      }
      else // Sonst (Fenster vorbei ODER PID=0 ODER Feature verhindert Heizen)
      {
          digitalWrite(SSR_WASSER_PIN, LOW);
      }
  }

  // --- Dampf SSR ---
  // Heizen wird gestoppt, wenn ein Sensorfehler ODER eine Sicherheitsabschaltung vorliegt
  if (dampfSensorError || dampfSafetyShutdown)
  {
      digitalWrite(SSR_DAMPF_PIN, LOW); // Sicherheit: Bei Fehler/Shutdown immer AUS
  }
  else // Nur heizen, wenn KEIN Fehler UND KEIN Shutdown
  {
      // Fenster neu starten, wenn Zeit abgelaufen
      if (now - windowStartTimeDampf > windowSizeDampf)
      {
          windowStartTimeDampf += windowSizeDampf;
      }

      // Prüfen, ob geheizt werden soll/darf
      // Bedingung 1: PID/Boost will heizen (innerhalb des Zeitfensters)
      // Beachte: OutputDampf kann durch Startverzögerung auf 0 sein
      bool pidWantsHeat = (OutputDampf > 0) && (now < (windowStartTimeDampf + (unsigned long)OutputDampf));
      // Bedingung 2: Überhitzungsschutz-Feature aktiv UND Temperatur ist bereits über Sollwert
      bool preventOverheatActive = preventHeatAboveSetpointDampf && (InputDampf > SetpointDampf);

      // Nur heizen, wenn Bedingung 1 wahr ist UND Bedingung 2 falsch ist
      if (pidWantsHeat && !preventOverheatActive)
      {
          digitalWrite(SSR_DAMPF_PIN, HIGH);
      }
      else // Sonst (Fenster vorbei ODER PID=0 ODER Feature verhindert Heizen)
      {
          digitalWrite(SSR_DAMPF_PIN, LOW);
      }
  }

  // Shot-Timer verarbeiten (unverändert)
  updateShotTimer();

  // Eco-Modus (unverändert - Sicherheitschecks haben Vorrang vor Eco-Sollwerten)
  if (!autoTuneWasserActive && !autoTuneDampfActive)
  {
    if (ecoModeMinutes > 0 && !shotActive)
    {
      unsigned long currentTimeEco = millis();
      if (currentTimeEco - lastShotTime > (ecoModeMinutes * 60UL * 1000UL))
      {
        if (!ecoModeAktiv) { ecoModeActivatedTime = currentTimeEco; }
        ecoModeAktiv = true;
        if (dynamicEcoActive)
        {
          int minutesPassed = (currentTimeEco - ecoModeActivatedTime) / (60UL * 1000UL);
          SetpointWasser = (ecoModeTempWasser - minutesPassed > 0 ? ecoModeTempWasser - minutesPassed : 0);
          SetpointDampf  = (ecoModeTempDampf  - minutesPassed > 0 ? ecoModeTempDampf  - minutesPassed : 0);
        }
        else
        {
          SetpointWasser = ecoModeTempWasser;
          SetpointDampf  = ecoModeTempDampf;
        }
      }
      else
      {
        // Wenn Eco-Zeit noch nicht erreicht ODER Shot aktiv war, normale Sollwerte laden
        // Nur laden, wenn Eco gerade deaktiviert wird, um ständiges EEPROM-Lesen zu vermeiden
        if (ecoModeAktiv) {
             EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
             EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
        }
        ecoModeAktiv = false;
        ecoModeActivatedTime = 0;
      }
    } else if (ecoModeAktiv) {
        // Sicherstellen, dass Eco deaktiviert wird, wenn ecoModeMinutes=0 gesetzt wird o.ä.
        ecoModeAktiv = false;
        ecoModeActivatedTime = 0;
        EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
        EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
    }
  }

  // Display regelmäßig updaten (Logik für Wartungsmeldung hinzugefügt)
  updateDisplay();
  server.handleClient(); // Webserver (unverändert)
  MDNS.update(); // mDNS-Anfragen bearbeiten

  // Betriebszeit zählen (unverändert)
  static unsigned long lastRuntimeSecond = 0;
  if (millis() - lastRuntimeSecond >= 1000)
  {
    totalRuntime++;
    lastRuntimeSecond = millis();
  }

  // Speichern der Betriebszeit (NEUE LOGIK) und WiFi-Check - Unverändert
  if (!demoModus) // Bedingung hinzugefügt: Nur speichern, wenn KEIN Demo-Modus aktiv ist
  {
      unsigned long currentMillisLoop = millis(); // Aktuelle Zeit für diese Prüfung holen
      unsigned long intervalToUse;

      // Bestimme, welches Intervall verwendet werden soll
      if (!initialRuntimeSaveDone) {
          // Wenn das erste Speichern noch nicht erfolgt ist, nutze die 10-Minuten-Verzögerung
          intervalToUse = initialRuntimeSaveDelay;
      } else {
          // Wenn das erste Speichern bereits erfolgt ist, nutze das 5-Minuten-Intervall
          intervalToUse = subsequentRuntimeSaveInterval;
      }

      // Prüfe, ob das ermittelte Intervall seit dem letzten Speichern abgelaufen ist
      if (currentMillisLoop - lastRuntimeSave >= intervalToUse)
      {
          EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Schreibe die aktuelle Betriebszeit ins EEPROM
          EEPROM.commit(); // Führe das Schreiben physisch durch

          lastRuntimeSave = currentMillisLoop; // Aktualisiere den Zeitpunkt des letzten Speicherns

          // Wenn dies das erste Speichern war, setze das Flag
          if (!initialRuntimeSaveDone) {
              initialRuntimeSaveDone = true;
              Serial.println("Betriebszeit: Erstes Speichern nach 10 Minuten erfolgt."); // Optional: Log-Ausgabe
          } else {
              Serial.println("Betriebszeit: Regelmäßiges Speichern (alle 5 Minuten) erfolgt."); // Optional: Log-Ausgabe
          }

          // Den WiFi-Check können wir auch hier durchführen, da er nun seltener aufgerufen wird
          if (WiFi.getMode() == WIFI_STA) {
              checkWifiConnection();
          }
      }
  }
}

/************************************************************************************
 * Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC) - Unverändert
 ************************************************************************************/

double readNTCTemperature()
{
  int adcValue = analogRead(A0);
  // Prüfen auf ungültige ADC-Werte (können bei Problemen auftreten)
  if (adcValue <= 0 || adcValue >= 1023) // 1023 ist max. Wert, kann auf Kurzschluss hindeuten
  {
     Serial.print("WARNUNG: Ungültiger ADC-Wert vom NTC-Sensor: ");
     Serial.println(adcValue);
     return NAN; // Not-a-Number als Fehlerwert zurückgeben
  }

  double sensorVoltage = adcValue * (ADC_REF / 1023.0);
  // Vermeide Division durch Null, falls sensorVoltage == V_SUPPLY
  if (sensorVoltage >= V_SUPPLY) {
     Serial.println("WARNUNG: NTC-Spannung entspricht Versorgungsspannung, R_ntc wäre unendlich.");
     return NAN; // Fehler
  }
  // Vermeide Division durch Null, falls sensorVoltage == 0 (bereits oben abgefangen, aber zur Sicherheit)
  if (sensorVoltage <= 0) {
     Serial.println("WARNUNG: NTC-Spannung ist 0 oder negativ.");
     return NAN; // Fehler
  }

  double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0);

  // Prüfen auf negativen oder sehr kleinen Widerstandswert
  if (R_ntc <= 0) {
     Serial.print("WARNUNG: Berechneter NTC-Widerstand ungültig: ");
     Serial.println(R_ntc);
     return NAN; // Fehler
  }

  // Temperaturberechnung über die Beta-Gleichung
  // log() erwartet einen positiven Wert
  double log_R_R0 = log(R_ntc / R0);
  double temp_inv_K = (1.0 / T0) + (1.0 / beta) * log_R_R0;

  // Prüfen auf Division durch Null bei der Invertierung
  if (temp_inv_K == 0) {
     Serial.println("WARNUNG: Kehrwert der Kelvin-Temperatur ist 0.");
     return NAN; // Fehler
  }

  double temperatureK = 1.0 / temp_inv_K;
  double temperatureC = temperatureK - 273.15;

  // Optional: Plausibilitätsprüfung des Ergebnisses
  // if (temperatureC < -50 || temperatureC > 200) { // Beispielgrenzen
  //   Serial.print("WARNUNG: Berechnete NTC-Temperatur unplausibel: ");
  //   Serial.println(temperatureC);
  //   return NAN;
  // }

  return temperatureC;
}


/************************************************************************************
 * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst
 ************************************************************************************/

/************************************************************************************
 * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst
 ************************************************************************************/

/************************************************************************************
 * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst
 * Priorität: PID Tuning > Fast Heat Up > Aktiver Shot > Letzte Shot-Zeit > Wartung > Temperaturen
 ************************************************************************************/

/************************************************************************************
 * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst
 * Priorität: PID Tuning > Fast Heat Up > Aktiver Shot > Letzte Shot-Zeit > Wartung > Temp/Shutdown/Fehler
 ************************************************************************************/
void updateDisplay()
{
  // 1. Höchste Priorität: PID Tuning aktiv? -> Eigene Anzeige, dann Ende.
  if (autoTuneWasserActive || autoTuneDampfActive) {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    display.println(F("PID-Tuning aktiv:"));
    display.println(autoTuneWasserActive ? F("Wasser-PID") : F("Dampf-PID"));
    display.println("");

    // Statusvariablen abfragen und anzeigen
    int currentPeakCount = 0;
    int currentPeakType = 0;
    if (autoTuneWasserActive && autoTuneWasser != nullptr) { // Sicherstellen, dass Objekt existiert
        currentPeakCount = autoTuneWasser->getPeakCount();
        currentPeakType = autoTuneWasser->getPeakType();
    } else if (autoTuneDampfActive && autoTuneDampf != nullptr) { // Sicherstellen, dass Objekt existiert
        currentPeakCount = autoTuneDampf->getPeakCount();
        currentPeakType = autoTuneDampf->getPeakType();
    }
    display.print(F("Cycle: "));
    if(currentPeakCount < 0 || currentPeakCount > 10) 
      { 
        // Falls kleiner 0 oder größer 10 (kann bei Start vorkommen)
        display.print(F("0"));
      } else {
        // Zeigt an, wie viele Zyklen/Phasen erkannt wurden
        display.print(currentPeakCount);
      }
        
    display.print(F(" von 5-10"));
    display.println(); // Zeilenumbruch
    display.print(F("Phase:"));
    if (currentPeakType == 1) {
        display.print(F("Heizen")); // Letztes war/suche Maximum
    } else if (currentPeakType == -1) {
        display.print(F("K\201hlen")); // Letztes war/suche Minimum
    } else {
        display.print(F("Init")); // Initialisierungsphase
    }
    display.println(); // Zeilenumbruch
    if(autoTuneWasserActive) 
      {
        display.print("Temperatur:");
        display.print((int)InputWasser);
        display.print(F(" "));
        display.print((char)247); // Grad-Symbol
        display.print(F("C"));
      }
    if(autoTuneDampfActive) 
      {
        display.print("Temperatur:");
        display.print((int)InputDampf);
        display.print(F(" "));
        display.print((char)247); // Grad-Symbol
        display.print(F("C"));
      }
    // WiFi Symbol trotzdem anzeigen, falls verbunden
    if (WiFi.status() == WL_CONNECTED)
    {
        drawwifiSymbol(113, 0);
    }
    display.display();
    return; // Funktion hier beenden
  }

  // --- Keine Tuning-Anzeige ---
  display.clearDisplay();

  // WiFi Symbol (immer anzeigen, wenn verbunden)
  if (WiFi.status() == WL_CONNECTED)
  {
    drawwifiSymbol(113, 0);
  }

  display.setCursor(0, 0);
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);

  // 2. Priorität: Fast-Heat-Up aktiv und relevant?
  if (fastHeatUpHeating && !shotActive && !wasserSensorError && ((int)InputWasser < fastHeatUpSetpoint))
  {
    SetpointWasser = fastHeatUpSetpoint; // Setpoint temporär anpassen für Anzeige
    display.println(F("Fast-Heat-Up aktiv:"));
    display.println("");
    display.println(F("20 Sek. Flushen,"));
    display.println(F("wenn Temp. erreicht!"));
    display.println(F(" ")); // Leere Zeile
    display.println(F("Temperatur: ")); 
    display.print((int)InputWasser);
    display.print(F(" / "));
    display.print((int)SetpointWasser); // Zeigt den F-H-U Setpoint
    display.print(F(" "));
    display.print((char)247); // Grad-Symbol
    display.print(F("C"));
    // Hier keine Dampfanzeige während Fast-Heat-Up, um Platz zu sparen
  }
  // 3. Kein Fast-Heat-Up: Was soll sonst angezeigt werden?
  else
  {
    // 3a. Priorität: Shot läuft *aktuell*?
    if (shotActive)
    {
      unsigned long elapsed = millis() - shotStartTime;
      display.println(F("Shot-Timer:"));
      display.println("");
      display.setTextSize(3); // Größere Schrift für den Timer
      // Zeige nur eine Nachkommastelle an
      char buffer[6]; // Puffer für "xx.y" + Nullterminator
      snprintf(buffer, sizeof(buffer), "%.1f", elapsed / 1000.0);
      display.println(buffer);
      display.setTextSize(1); // Zurück zur normalen Schriftgröße
      display.println(F("Sekunden"));
      fastHeatUpHeating = false; // Laufender Shot beendet F-H-U Anzeige visuell
    }
    // 3b. Priorität: Letzte Shot-Zeit anzeigen (für 2 Sekunden)?
    else if (shotEndTime > 0 && millis() < shotEndTime + 2000)
    {
        display.println(F("Bezugszeit:"));
        display.println("");
        display.setTextSize(3); // Größere Schrift
        // Zeige nur eine Nachkommastelle an
        char buffer[6];
        snprintf(buffer, sizeof(buffer), "%.1f", lastShotDurationMillis / 1000.0);
        display.println(buffer);
        display.setTextSize(1); // Zurück zur normalen Schriftgröße
        display.println(F("Sekunden"));
    }
    // 3c. Priorität: Wartungserinnerung anzeigen (für 6 Sekunden nach Trigger)
    // Diese Bedingung wird nur erreicht, wenn kein Shot aktiv ist UND die letzte Shot-Zeit nicht angezeigt wird.
    else if (displayMaintenanceMessage && millis() < maintenanceMessageStartTime + 6000)
    {
        display.println(F("Erinnerung:")); // Titel
        display.println(F("Reinigung /")); // Zeile 1
        display.println(F("Wartung fällig!")); // Zeile 2
        // Restliche Zeilen leer lassen oder für Details nutzen
        display.println("");
        display.println("");
        display.println("");
    }
    // 3d. Standardanzeige: Temperaturen / Fehler / Sicherheitsabschaltung
    else
    {
        display.println(F("Temperaturen:")); // Titelzeile
        display.println(""); // Leerzeile

        // Wasser Temperatur / Sicherheitsabschaltung / Fehleranzeige
        display.println(F("Wasser: ")); // Print, kein Zeilenumbruch
        if (wasserSafetyShutdown) { // Prüfung 1: Sicherheitsabschaltung
            display.print(F("Sicherheitsabsch."));
        } else if (wasserSensorError) { // Prüfung 2: Echter Sensorfehler
            display.print(F("Sensorfehler!"));
        } else { // Prüfung 3: Alles OK -> Temperatur anzeigen
            display.print((int)InputWasser);
            display.print(F(" / "));
            display.print((int)SetpointWasser);
            display.print(F(" "));
            display.print((char)247); // Grad-Symbol
            display.print(F("C"));
            // ECO-Modus Anzeige anhängen, falls aktiv
            if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)"));
            else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)"));
        }
        display.println(""); // Zeilenumbruch nach der Wasserzeile
        display.println(""); // Leere Zeile für Abstand

        // Dampf Temperatur / Sicherheitsabschaltung / Fehleranzeige
        display.println(F("Dampf:  ")); // Print, kein Zeilenumbruch, 2 Leerzeichen für Ausrichtung
         if (dampfSafetyShutdown) { // Prüfung 1: Sicherheitsabschaltung
            display.print(F("Sicherheitsabsch."));
        } else if (dampfSensorError) { // Prüfung 2: Echter Sensorfehler
            display.print(F("Sensorfehler!"));
        } else { // Prüfung 3: Alles OK -> Temperatur anzeigen (oder Verzögerung)
            // Prüfen auf Startverzögerung (wie vorher)
            bool steamDelayActive = (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL));
            if (!steamDelayActive) // Wenn keine Verzögerung aktiv ist
            {
                display.print((int)InputDampf);
                display.print(F(" / "));
                display.print((int)SetpointDampf);
                display.print(F(" "));
                display.print((char)247); // Grad-Symbol
                display.print(F("C"));
                 // ECO-Modus Anzeige anhängen, falls aktiv
                if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)"));
                else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)"));
            } else { // Wenn Startverzögerung aktiv ist
                display.print(F("Startverz\x94gerung"));
            }
        }
        // Kein println() am Ende der letzten Zeile nötig
    } // Ende Standardanzeige (Temperaturen etc.)
  } // Ende "Kein Fast-Heat-Up"

  display.display(); // Display *immer* am Ende aktualisieren, egal was angezeigt wurde
}

/************************************************************************************
 * Verwaltet den Shot-Timer (wenn z.B. der Nutzer den Bezug aktiviert) - Wartungscheck hinzugefügt
 ************************************************************************************/

void updateShotTimer()
{
  if (digitalRead(SHOT_TIMER_PIN) == HIGH) // Annahme: HIGH = Bezug aktiv
  {
    // Shot beginnt
    if (!shotActive)
    {
      shotActive = true;
      shotStartTime = millis();
      lastShotDurationMillis = 0; // Dauer für neuen Shot zurücksetzen
      shotEndTime = 0;           // Endzeit für neuen Shot zurücksetzen
      displayMaintenanceMessage = false; // Sicherstellen, dass alte Meldung weg ist
    }
  }
  else // Annahme: LOW = Bezug inaktiv
  {
    // Shot endet
    if (shotActive)
    {
      unsigned long shotDuration = millis() - shotStartTime;
      lastShotDurationMillis = shotDuration; // *** Letzte Dauer speichern ***
      shotEndTime = millis();              // *** Endzeitpunkt speichern ***

      // nur Shots > 20 Sekunden zählen
      if (shotDuration > 20000)
      {
        shotCounter++;
        maintenanceIntervalCounter++;
        if (!demoModus) { // Nur speichern, wenn kein Demo-Modus
          EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
          EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter);
          EEPROM.commit();
        }
        Serial.print(F("Shots: "));
        Serial.println(shotCounter);

        // Prüfen, ob Wartungsintervall erreicht ist
        if (maintenanceInterval > 0 && maintenanceIntervalCounter >= maintenanceInterval) {
          displayMaintenanceMessage = true; // Flag für Anzeige setzen
          maintenanceMessageStartTime = millis(); // Startzeit für Anzeige merken
          Serial.println("Wartungsintervall erreicht!");
        }
      }
      shotActive = false;     // Shot als beendet markieren
      lastShotTime = millis(); // Wichtig für Eco-Modus (bleibt wie es ist)
      // shotEndTime und delayDisplayUpdate werden nicht mehr benötigt (bzw. shotEndTime hat neue Bedeutung)
    }
  }
}

/************************************************************************************
 * Startet AutoTune für Wasser-PID (Angepasst für Zeitproportional)
 ************************************************************************************/

void startAutoTuneWasser()
{
  // Eco-Modus deaktivieren, falls aktiv
  if(ecoModeAktiv)
  {
    ecoModeAktiv = false;
    // Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil)
    EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
    // Optional: Auch Dampf-Setpoint wiederherstellen, falls Eco beide beeinflusst
    EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
  }

  // Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt
  pidWasser.SetMode(AUTOMATIC);

  // Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind
  pidWasser.SetOutputLimits(0, windowSizeWasser);

  autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser);
  // Verwende die Wasser-spezifischen Parameter
  autoTuneWasser->SetOutputStep(tuningStepWasser);       // Angepasst
  autoTuneWasser->SetControlType(1); // DIRECT control (heating)
  autoTuneWasser->SetNoiseBand(tuningNoiseWasser);       // Angepasst
  autoTuneWasser->SetLookbackSec((int)tuningLookBackWasser); // Angepasst (Cast zu int für Funktion)

  // Setze den Startwert für den Output
  // WICHTIG: AutoTune manipuliert OutputWasser direkt. Die zeitproportionale
  // Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern.
  OutputWasser = tuningStartValueWasser;            // Angepasst

  // KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das.

  autoTuneWasserActive = true;
  Serial.println("Starte AutoTune für Wasser (Zeitproportional)...");
}

/************************************************************************************
 * Startet AutoTune für Dampf-PID (Angepasst für Zeitproportional)
 ************************************************************************************/

void startAutoTuneDampf()
{
  // Eco-Modus deaktivieren, falls aktiv
  if(ecoModeAktiv)
  {
    ecoModeAktiv = false;
    // Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil)
    EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
     // Optional: Auch Wasser-Setpoint wiederherstellen, falls Eco beide beeinflusst
    EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
  }

  // Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt
  pidDampf.SetMode(AUTOMATIC);

  // Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind
  pidDampf.SetOutputLimits(0, windowSizeDampf);

  autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf);
  // Verwende die Dampf-spezifischen Parameter
  autoTuneDampf->SetOutputStep(tuningStepDampf);        // Angepasst
  autoTuneDampf->SetControlType(1); // DIRECT control (heating)
  autoTuneDampf->SetNoiseBand(tuningNoiseDampf);        // Angepasst
  autoTuneDampf->SetLookbackSec((int)tuningLookBackDampf); // Angepasst (Cast zu int für Funktion)

  // Setze den Startwert für den Output
  // WICHTIG: AutoTune manipuliert OutputDampf direkt. Die zeitproportionale
  // Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern.
  OutputDampf = tuningStartValueDampf;            // Angepasst

  // KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das.

  autoTuneDampfActive = true;
  Serial.println("Starte AutoTune für Dampf (Zeitproportional)...");
}

/************************************************************************************
 * Stoppt AutoTune für Wasser - Unverändert
 ************************************************************************************/

void stopAutoTuneWasser()
{
  if (autoTuneWasserActive)
  {
    autoTuneWasserActive = false;
    delete autoTuneWasser;
    autoTuneWasser = nullptr;
    // Setze PID zurück in den Normalbetrieb (ggf. Output auf 0)
    pidWasser.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft
    OutputWasser = 0; // Optional: Output zurücksetzen
    // Stelle sicher, dass SSR ausgeschaltet ist
    digitalWrite(SSR_WASSER_PIN, LOW);
    Serial.println("AutoTune Wasser gestoppt.");
  }
}

/************************************************************************************
 * Stoppt AutoTune für Dampf - Unverändert
 ************************************************************************************/

void stopAutoTuneDampf()
{
  if (autoTuneDampfActive)
  {
    autoTuneDampfActive = false;
    delete autoTuneDampf;
    autoTuneDampf = nullptr;
    // Setze PID zurück in den Normalbetrieb (ggf. Output auf 0)
    pidDampf.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft
    OutputDampf = 0; // Optional: Output zurücksetzen
    // Stelle sicher, dass SSR ausgeschaltet ist
    digitalWrite(SSR_DAMPF_PIN, LOW);
    Serial.println("AutoTune Dampf gestoppt.");
  }
}

/************************************************************************************
 * Überwacht die AutoTune-Prozesse (Dampf/Wasser)
 * - Prüft auf Sensorfehler ODER Sicherheitsabschaltung vor Ausführung
 * - Display-Logik ist in updateDisplay() ausgelagert
 ************************************************************************************/
void handleAutoTune()
{
  // --- AutoTune Wasser ---
  if (autoTuneWasserActive)
  {
    // Erweiterte Prüfung auf Sensorfehler ODER Sicherheitsabschaltung während des Tunings
    if (wasserSensorError || wasserSafetyShutdown) {
        // Logge den spezifischen Grund für den Abbruch
        if (wasserSensorError) {
             Serial.println("FEHLER: Wassertemperatursensor während AutoTune ausgefallen! Breche Tuning ab.");
        } else { // Muss wasserSafetyShutdown sein
             Serial.println("WARNUNG: Sicherheitslimit Wasser während AutoTune überschritten! Breche Tuning ab.");
        }
        stopAutoTuneWasser(); // Ruft Funktion auf, die Flag zurücksetzt, Speicher freigibt und SSR ausschaltet
        return; // Verlasse die Funktion für diesen Durchlauf, um Runtime() nicht auszuführen
    }

    // Nur wenn kein Fehler/Shutdown vorliegt, führe AutoTune->Runtime() aus.
    // Diese Funktion berechnet und SETZT OutputWasser für den nächsten Schritt!
    if (autoTuneWasser->Runtime() == 1) // Runtime() gibt 1 zurück, wenn Tuning abgeschlossen
    {
      // Tuning abgeschlossen, Werte holen
      KpWasser = autoTuneWasser->GetKp();
      KiWasser = autoTuneWasser->GetKi();
      KdWasser = autoTuneWasser->GetKd();

      // WICHTIG: PID-Tunings sofort anwenden
      pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);

      // Neue Werte im EEPROM speichern
      EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
      EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
      EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
      EEPROM.commit();

      Serial.println("AutoTune Wasser erfolgreich abgeschlossen.");
      Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpWasser, KiWasser, KdWasser);

      // AutoTune-Prozess korrekt beenden
      stopAutoTuneWasser();
    }
    // Hinweis: Die Display-Anzeige für laufendes Tuning wird in updateDisplay() gehandhabt.
  }

  // --- AutoTune Dampf ---
  if (autoTuneDampfActive)
  {
     // Erweiterte Prüfung auf Sensorfehler ODER Sicherheitsabschaltung während des Tunings
    if (dampfSensorError || dampfSafetyShutdown) {
       // Logge den spezifischen Grund für den Abbruch
       if (dampfSensorError) {
           Serial.println("FEHLER: Dampftemperatursensor während AutoTune ausgefallen! Breche Tuning ab.");
       } else { // Muss dampfSafetyShutdown sein
           Serial.println("WARNUNG: Sicherheitslimit Dampf während AutoTune überschritten! Breche Tuning ab.");
       }
       stopAutoTuneDampf(); // Ruft Funktion auf, die Flag zurücksetzt, Speicher freigibt und SSR ausschaltet
       return; // Verlasse die Funktion für diesen Durchlauf, um Runtime() nicht auszuführen
    }

    // Nur wenn kein Fehler/Shutdown vorliegt, führe AutoTune->Runtime() aus.
    // Diese Funktion berechnet und SETZT OutputDampf für den nächsten Schritt!
    if (autoTuneDampf->Runtime() == 1) // Runtime() gibt 1 zurück, wenn Tuning abgeschlossen
    {
      // Tuning abgeschlossen, Werte holen
      KpDampf = autoTuneDampf->GetKp();
      KiDampf = autoTuneDampf->GetKi();
      KdDampf = autoTuneDampf->GetKd();

      // WICHTIG: PID-Tunings sofort anwenden
      pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);

      // Neue Werte im EEPROM speichern
      EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
      EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
      EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
      EEPROM.commit();

      Serial.println("AutoTune Dampf erfolgreich abgeschlossen.");
      Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpDampf, KiDampf, KdDampf);

      // AutoTune-Prozess korrekt beenden
      stopAutoTuneDampf();
    }
    // Hinweis: Die Display-Anzeige für laufendes Tuning wird in updateDisplay() gehandhabt.
  }
}

/************************************************************************************
 * Handler für die Info-Seite - Heap-Optimiert mit Chunked Sending
 ************************************************************************************/

// --- PROGMEM Chunks für handleInfo ---
// Kopfzeile bleibt gleich wie im Original `infoHtml`
static const char infoHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Info</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
// </head> bleibt gleich wie im Original `infoHtml2`
static const char infoHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

// Aufteilung von infoHtml3 in Chunks
static const char infoChunk_BodyStart[] PROGMEM = R"rawliteral(
<body>
  <h1>Info</h1>
  <form action='/updateInfoSettings' method='POST'>
    <h3>Ger&auml;teinfo</h3>
    Die Ger&auml;teinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.
    <br><br>
    <label for='hersteller'>Hersteller:</label>
    <input type='text' id='hersteller' name='hersteller' value=')rawliteral"; // Endet vor {HERSTELLER}
static const char infoChunk_AfterHersteller[] PROGMEM = R"rawliteral('>
    <label for='modell'>Modell:</label>
    <input type='text' id='modell' name='modell' value=')rawliteral"; // Endet vor {MODELL}
static const char infoChunk_AfterModell[] PROGMEM = R"rawliteral('>
    <label for='zusatz'>Zusatz (z.B. Limited Edition):</label>
    <input type='text' id='zusatz' name='zusatz' value=')rawliteral"; // Endet vor {ZUSATZ}
static const char infoChunk_AfterZusatz[] PROGMEM = R"rawliteral('>
    <input type='submit' value='Einstellungen speichern'>
  </form>

  <form action='/updateMaintenanceSettings' method='POST'>
    <h3>Reinigung & Wartung</h3>
    Zeigt nach der eingestellten Anzahl von Bez&uuml;gen eine Reinigungs-/ Wartungserinnerung im Display an.<br>
    <label for='maintenanceInterval'>Anzahl an Bez&uuml;gen bis Erinnerung: (0 = deaktiviert)</label>
    <input type='text' id='maintenanceInterval' name='maintenanceInterval' value=')rawliteral"; // Endet vor {MAINTENANCE_INTERVAL}
static const char infoChunk_AfterMaintInterval[] PROGMEM = R"rawliteral('>
    <input type='submit' value='Einstellungen speichern'>
  </form>

  <form action='/resetMaintenance' method='POST'>
    <h3>Wartungsz&auml;hler zur&uuml;cksetzen</h3>
    Aktueller Z&auml;hlerstand: )rawliteral"; // Endet vor {MAINTENANCECOUNTER}
static const char infoChunk_AfterMaintCounter[] PROGMEM = R"rawliteral(<br>
    <br>
    Setzt den Z&auml;hler f&uuml;r die Reinigungs- & Wartungserinnerung zur&uuml;ck.<br>
    Dies ist nach der erfolgreichen Durchf&uuml;hrung notwendig,<br>
    damit die Meldung nicht nach jedem Bezug erneut scheint.<br>
    <input type='submit' value='Zur&uuml;cksetzen'>
  </form>

  <form action='/resetRuntime' method='POST'>
    <h3>Betriebszeit:</h3>
)rawliteral"; // Endet vor {RUNTIME}
static const char infoChunk_AfterRuntime[] PROGMEM = R"rawliteral(<br>
    <input type='submit' value='Zur&uuml;cksetzen'>
  </form>
  <form action='/resetShots' method='POST'>
    <h3>Shots: (Bez&uuml;ge &uuml;ber 20 Sekunden)</h3>
    Anzahl: )rawliteral"; // Endet vor {SHOT_COUNT}
static const char infoChunk_AfterShotCount[] PROGMEM = R"rawliteral(<br>
    <input type='submit' value='Zur&uuml;cksetzen'>
  </form>
  <form>
    <h3>Systeminfo</h3>
    Freier Heap: )rawliteral"; // Endet vor {FREE_HEAP}
static const char infoChunk_AfterFreeHeap[] PROGMEM = R"rawliteral( Bytes<br>
    SDK-Version: )rawliteral"; // Endet vor {SDK_VERSION}
static const char infoChunk_AfterSDK[] PROGMEM = R"rawliteral(<br>
    Core-Version: )rawliteral"; // Endet vor {CORE_VERSION}
static const char infoChunk_AfterCore[] PROGMEM = R"rawliteral(<br>
    Boot-Version: )rawliteral"; // Endet vor {BOOT_VERSION}
static const char infoChunk_AfterBoot[] PROGMEM = R"rawliteral(<br>
    Chip-ID: )rawliteral"; // Endet vor {CHIP_ID}
static const char infoChunk_AfterChipID[] PROGMEM = R"rawliteral(<br>
    CPU-Takt: )rawliteral"; // Endet vor {CPU_MHZ}
static const char infoChunk_AfterCPU[] PROGMEM = R"rawliteral( MHz<br>
    Letzter Reset-Grund: )rawliteral"; // Endet vor {RESET_REASON}
static const char infoChunk_AfterResetReason[] PROGMEM = R"rawliteral(<br>
    <br>
    Sketch-Gr&ouml;&szlig;e: )rawliteral"; // Endet vor {SKETCH_SIZE}
static const char infoChunk_AfterSketchSize[] PROGMEM = R"rawliteral( Bytes<br>
    Freier Sketch-Speicher: )rawliteral"; // Endet vor {FREE_SKETCH}
static const char infoChunk_AfterFreeSketch[] PROGMEM = R"rawliteral( Bytes<br>
    Flash-Gr&ouml;&szlig;e (Chip): )rawliteral"; // Endet vor {FLASH_SIZE}
static const char infoChunk_End[] PROGMEM = R"rawliteral( Bytes<br>
   </form>
 </body>
 </html>
)rawliteral";

void handleInfo() // URL: /Info
{
  char buffer[40]; // Allzweck-Puffer für Zahlen etc.
  // Temp Puffer für String-Kopien, Größe = max. erwartete Länge (z.B. von infoHersteller)
  char tempStringCopyBuffer[50];

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(infoHtmlHead);
  server.sendContent_P(commonStyle);
  server.sendContent_P(infoHtmlHeadEnd);
  server.sendContent_P(commonNav);

  // Body Start bis Hersteller
  server.sendContent_P(infoChunk_BodyStart);
  // Hersteller Wert - Nur senden, wenn nicht leer
  if (strlen(infoHersteller) > 0) {
      server.sendContent(infoHersteller);
  }
  // Nach Hersteller bis Modell
  server.sendContent_P(infoChunk_AfterHersteller);
  // Modell Wert - Nur senden, wenn nicht leer
  if (strlen(infoModell) > 0) {
      server.sendContent(infoModell);
  }
  // Nach Modell bis Zusatz
  server.sendContent_P(infoChunk_AfterModell);

  // === ANPASSUNG FÜR LEERE STRINGS ===
  // Zusatz Wert - Nur senden, wenn nicht leer
  // (Kopie zur Sicherheit gegen fehlende Null-Terminierung beibehalten)
  strncpy(tempStringCopyBuffer, infoZusatz, sizeof(tempStringCopyBuffer) - 1);
  tempStringCopyBuffer[sizeof(tempStringCopyBuffer) - 1] = '\0';
  if (strlen(tempStringCopyBuffer) > 0) {
      server.sendContent(tempStringCopyBuffer);
  }
  // === ENDE ANPASSUNG ===

  // Nach Zusatz bis Wartungsintervall
  server.sendContent_P(infoChunk_AfterZusatz);

  // --- REST DER FUNKTION (mit Checks für Core/ResetReason) ---
  // Wartungsintervall Wert
  snprintf(buffer, sizeof(buffer), "%d", maintenanceInterval);
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterMaintInterval);
  // Wartungszähler Wert
  snprintf(buffer, sizeof(buffer), "%lu", maintenanceIntervalCounter);
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterMaintCounter);

  // Betriebszeit berechnen und stückweise senden
  unsigned long totalSeconds = totalRuntime;
  unsigned long days    = totalSeconds / 86400;
  unsigned long hours   = (totalSeconds % 86400) / 3600;
  unsigned long minutes = (totalSeconds % 3600) / 60;
  snprintf(buffer, sizeof(buffer), "%lu", days); server.sendContent(buffer);
  server.sendContent(F(" Tag(e), "));
  snprintf(buffer, sizeof(buffer), "%lu", hours); server.sendContent(buffer);
  server.sendContent(F(" Stunde(n) und "));
  snprintf(buffer, sizeof(buffer), "%lu", minutes); server.sendContent(buffer);
  server.sendContent(F(" Minuten"));

  server.sendContent_P(infoChunk_AfterRuntime);
  // Shot Counter Wert
  snprintf(buffer, sizeof(buffer), "%lu", shotCounter);
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterShotCount);
  // Free Heap Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getFreeHeap());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterFreeHeap);
  // SDK Version (ist const char*) - Auch hier auf Leerstring prüfen
  const char* sdkVersion = ESP.getSdkVersion();
  if (sdkVersion && strlen(sdkVersion) > 0) {
      server.sendContent(sdkVersion);
  } else if (!sdkVersion) { // Falls NULL zurückkommt
      server.sendContent(F("N/A"));
  } // Wenn leer, wird nichts gesendet
  server.sendContent_P(infoChunk_AfterSDK);
  // Core Version (String -> Sicher kopieren & prüfen)
  String coreVersionStr = ESP.getCoreVersion();
  strncpy(tempStringCopyBuffer, coreVersionStr.c_str(), sizeof(tempStringCopyBuffer) - 1); tempStringCopyBuffer[sizeof(tempStringCopyBuffer) - 1] = '\0';
  if (strlen(tempStringCopyBuffer) > 0) server.sendContent(tempStringCopyBuffer);
  server.sendContent_P(infoChunk_AfterCore);
  // Boot Version Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getBootVersion());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterBoot);
  // Chip ID Wert (als HEX)
  snprintf(buffer, sizeof(buffer), "%08X", ESP.getChipId());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterChipID);
  // CPU Takt Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getCpuFreqMHz());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterCPU);
  // Reset Grund (String -> Sicher kopieren & prüfen)
  String resetReasonStr = ESP.getResetReason();
  strncpy(tempStringCopyBuffer, resetReasonStr.c_str(), sizeof(tempStringCopyBuffer) - 1); tempStringCopyBuffer[sizeof(tempStringCopyBuffer) - 1] = '\0';
  if (strlen(tempStringCopyBuffer) > 0) server.sendContent(tempStringCopyBuffer);
  server.sendContent_P(infoChunk_AfterResetReason);
  // Sketch Größe Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getSketchSize());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterSketchSize);
  // Freier Sketch Speicher Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getFreeSketchSpace());
  server.sendContent(buffer);
  server.sendContent_P(infoChunk_AfterFreeSketch);
  // Flash Größe Wert
  snprintf(buffer, sizeof(buffer), "%u", ESP.getFlashChipSize());
  server.sendContent(buffer);
  // Ende der Seite
  server.sendContent_P(infoChunk_End);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

/************************************************************************************
 * Handler für die Eco-Modus-Konfiguration - Heap-Optimiert mit Chunked Sending
 ************************************************************************************/

// --- PROGMEM Chunks für handleEco ---
// Kopfzeile bleibt gleich wie im Original `ecoHtml`
static const char ecoHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Eco-Modus</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
// </head> bleibt gleich wie im Original `ecoHtml2`
static const char ecoHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

// Aufteilung von ecoHtml3 in Chunks
static const char ecoChunk_BodyStart[] PROGMEM = R"rawliteral(
<body>
  <h1>Eco-Modus</h1>
  <form action='/updateEcoSettings' method='POST'>
    <h3>Eco-Modus</h3>
    Der Eco-Modus senkt die voreingestellten Temperaturen nach der angegebenen Zeit auf die Eco-Temperaturen ab.<br>
    <label for='ecoMode'>Eco-Modus (Minuten, 0 = deaktiviert):</label>
    <input type='text' id='ecoMode' name='ecoMode' value=')rawliteral"; // Endet vor {ECO_MODE}
static const char ecoChunk_AfterEcoMode[] PROGMEM = R"rawliteral('>
    <label for='ecoModeTempWasser'>Eco-Temperatur Wasser:</label>
    <input type='text' id='ecoModeTempWasser' name='ecoModeTempWasser' value=')rawliteral"; // Endet vor {ECO_MODE_TEMP_WASSER}
static const char ecoChunk_AfterTempW[] PROGMEM = R"rawliteral('>
    <label for='ecoModeTempDampf'>Eco-Temperatur Dampf:</label>
    <input type='text' id='ecoModeTempDampf' name='ecoModeTempDampf' value=')rawliteral"; // Endet vor {ECO_MODE_TEMP_DAMPF}
static const char ecoChunk_AfterTempD[] PROGMEM = R"rawliteral('>
    <div style='margin-bottom:15px;'>
    <br>
    <h3>Dynamischer Eco-Modus</h3>
    <label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
    Dynamischen Eco-Modus aktivieren (ECO+):
    <input type='checkbox' id='dynamicEcoMode' name='dynamicEcoMode' value='1')rawliteral"; // Endet vor {DYNAMIC_ECO_MODE_CHECKBOX}
static const char ecoChunk_AfterDynCheck[] PROGMEM = R"rawliteral(>
    </label>
    </div>
    <div style='margin-top:15px;'>
      Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erh&auml;lt.<br>
      Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollst&auml;ndig beendet wird.<br>
      Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!<br>
      <br>
      <b>Beispiel f&uuml;r eine praktische Anwendung:</b><br>
      Peter hat an seiner Maschine den Eco-Modus auf 45 Minuten gestellt, mit Eco-Temperatur von 60 Grad. Nun kommt ihm etwas dazwischen und er kommt erst bei Minute 60 an die Maschine, um sich einen Espresso zuzubereiten. Die Maschine ist nun allerdings schon auf 60 Grad herabgek&uuml;hlt und er muss erneut das Aufheizen abwarten ...<br>
      Markus passiert das gleiche, doch er hat den dynamischen Eco-Modus mit einer initialen Eco-Temperatur von 95 Grad aktiviert - Seine Maschine ist nun zumindest noch bei 80 Grad.<br>
      Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.
    </div><br>
    <h3>Verz&ouml;gerung f&uuml;r Dampf</h3>
    <label for='dampfDelay'>Aufheizen f&uuml;r Dampf verz&ouml;gern (Minuten, 0 = deaktiviert):</label>
    <input type='text' id='dampfDelay' name='dampfDelay' value=')rawliteral"; // Endet vor {DAMPF_DELAY}
static const char ecoChunk_AfterDampfDelay[] PROGMEM = R"rawliteral('>
    <br><b style='color: #FFCC00;'>ACHTUNG:</b><br>
    Um Konflikte zu vermeiden, sollte der Wert geringer sein, als der des Eco-Modus, falls dieser aktiviert ist!
    <br>
    <input type='submit' value='Einstellungen speichern'>
  </form>
</body>
</html>
)rawliteral";


// --- Überarbeitete Funktion handleEco ---
void handleEco() // URL: /ECO
{
  char buffer[12]; // Puffer für Zahlenumwandlungen

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(ecoHtmlHead);
  server.sendContent_P(commonStyle);
  server.sendContent_P(ecoHtmlHeadEnd);
  server.sendContent_P(commonNav); // Navigation

  // Body Start bis Eco Mode Wert
  server.sendContent_P(ecoChunk_BodyStart);
  // Eco Mode Wert (int)
  snprintf(buffer, sizeof(buffer), "%d", ecoModeMinutes);
  server.sendContent(buffer);
  // Nach Eco Mode bis Eco Temp Wasser
  server.sendContent_P(ecoChunk_AfterEcoMode);
  // Eco Temp Wasser Wert (int)
  snprintf(buffer, sizeof(buffer), "%d", ecoModeTempWasser);
  server.sendContent(buffer);
  // Nach Eco Temp Wasser bis Eco Temp Dampf
  server.sendContent_P(ecoChunk_AfterTempW);
  // Eco Temp Dampf Wert (int)
  snprintf(buffer, sizeof(buffer), "%d", ecoModeTempDampf);
  server.sendContent(buffer);
  // Nach Eco Temp Dampf bis Dynamic Eco Checkbox
  server.sendContent_P(ecoChunk_AfterTempD);
  // Dynamic Eco checked Attribut (falls nötig)
  if (dynamicEcoActive) {
    server.sendContent(F(" checked"));
  }
  // Nach Dynamic Eco Checkbox bis Dampf Delay
  server.sendContent_P(ecoChunk_AfterDynCheck);
  // Dampf Delay Wert (int)
  snprintf(buffer, sizeof(buffer), "%d", dampfVerzoegerung);
  server.sendContent(buffer);
  // Rest der Seite
  server.sendContent_P(ecoChunk_AfterDampfDelay);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

/************************************************************************************
 * Handler für die Fast-Heat-Up Seite - Heap-Optimiert mit Chunked Sending
 ************************************************************************************/

// --- PROGMEM Chunks für handleFastHeatUp ---
// Kopfzeile bleibt gleich wie im Original `fhuHtml`
static const char fhuHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Fast-Heat-Up-Modus</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
// </head> bleibt gleich wie im Original `fhuHtml2`
static const char fhuHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

// Aufteilung von fhuHtml3 in Chunks
// Teil 1: Bis zum Ende des <input type='checkbox' ... value='1' Teils
static const char fhuChunk1[] PROGMEM = R"rawliteral(
<body>
  <h1>Fast-Heat-Up-Modus</h1>
  <form action='/updateFast-Heat-Up-Settings' method='POST'>
    <h3>Fast-Heat-Up-Modus</h3>
    <div style='margin-bottom:15px;'>
      <label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
        Fast-Heat-Up aktivieren:
      </label>
      <input type='checkbox' id='fast-heat-up-aktivieren' name='fastHeatUpAktiv' value='1')rawliteral"; // Endet direkt vor {FASTHEATUP_MODE_CHECKBOX}

// Teil 2: Vom '>' nach der Checkbox bis zum Ende
static const char fhuChunk2[] PROGMEM = R"rawliteral(>
    </div>
    <div style='margin-top:15px;'>
      Der Fast-Heat-Up-Modus erm&ouml;glicht es, die Maschine noch schneller aufzuheizen.<br>
      Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.<br>
      Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgef&uuml;hrt werden.<br>
      <br>
      <b style='color: #FFCC00;'>ACHTUNG:</b><br>
      Bitte beachten, dass auf der Seite PID-Einstellung eventuell die max. Wassertemperatur der Sicherheitsabschaltung angepasst werden muss!<br>
    </div>
    <input type='submit' value='Einstellungen speichern' style='margin-top:20px;'>
  </form>
</body>
</html>
)rawliteral";


// --- Überarbeitete Funktion handleFastHeatUp ---
void handleFastHeatUp() // URL: /Fast-Heat-Up
{
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(fhuHtmlHead);
  server.sendContent_P(commonStyle);
  server.sendContent_P(fhuHtmlHeadEnd);
  server.sendContent_P(commonNav); // Navigation

  // Chunk 1 senden (bis zur Checkbox)
  server.sendContent_P(fhuChunk1);

  // "checked" Attribut senden, falls Fast-Heat-Up aktiv ist
  if (fastHeatUpAktiv) {
    server.sendContent(F(" checked"));
  }

  // Chunk 2 senden (Rest der Seite)
  server.sendContent_P(fhuChunk2);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

/************************************************************************************
 * Handler zum Speichern der Fast-Heat-Up-Einstellung - URL angepasst
 ************************************************************************************/

void handleFastHeatUpSettings()
{
  if (server.hasArg(F("fastHeatUpAktiv")))
  {
    fastHeatUpAktiv = true;
  }
  else
  {
    fastHeatUpAktiv = false;
  }

  EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
  EEPROM.commit();

  server.sendHeader(F("Location"), F("/Fast-Heat-Up")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Handler für Root (PID-Einstellungen) - Heap-Optimiert mit Chunked Sending
 ************************************************************************************/

// --- PROGMEM Chunks für handleRoot ---
// Kopfzeile bleibt gleich wie im Original `rootHtml`
static const char rootHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>PID-Einstellung</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
// </head> bleibt gleich wie im Original `rootHtml2`
static const char rootHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

// Aufteilung von rootHtml3 in Chunks
static const char rootChunk_BodyStart[] PROGMEM = R"rawliteral(
<body>
  <h1>PID-Einstellung</h1>
)rawliteral";
// Nach {SENSOR_ERROR_MESSAGES} bis value='{WASSER}'
static const char rootChunk_FormStart[] PROGMEM = R"rawliteral(
  <form action='/updateSettings' method='POST'>
    <h3>Temperatur:  Wasser / Kessel</h3>
    <label for='wasser'>Setpoint (&deg;C):</label>
    <input type='text' id='wasser' name='wasser' value=')rawliteral";
// Nach {WASSER} bis value='{OFFSET_WASSER}'
static const char rootChunk_AfterWasser[] PROGMEM = R"rawliteral('>
    <label for='offsetWasser'>Offset (&deg;C):</label>
    <input type='text' id='offsetWasser' name='offsetWasser' value=')rawliteral";
// Nach {OFFSET_WASSER} bis {BOOST_WASSER_CHECKED}
static const char rootChunk_AfterOffsetW[] PROGMEM = R"rawliteral('>
    <label for='boostWasser' style='display:inline-block; margin-right:10px;'>
    Boost-Funktion:
    </label>
    <input type='checkbox' id='boostWasser' name='boostWasser' value='1')rawliteral"; // Endet direkt vor dem checked Attribut
// Nach {BOOST_WASSER_CHECKED} bis {PREVENTHEAT_WASSER_CHECKED}
static const char rootChunk_AfterBoostW[] PROGMEM = R"rawliteral(>
    <br>
    <small style='color: #ccc;'>(Volle Heizleistung erzwingen bis 95% der Zieltemperatur)</small>
    <br>
    <label for='preventHeatWasser' style='display:inline-block; margin-right:10px;'>
    Heizen oberhalb Setpoint verhindern:
    </label>
    <input type='checkbox' id='preventHeatWasser' name='preventHeatWasser' value='1')rawliteral"; // Endet direkt vor dem checked Attribut
// Nach {PREVENTHEAT_WASSER_CHECKED} bis value='{DAMPF}'
static const char rootChunk_AfterPreventW[] PROGMEM = R"rawliteral(>
    <br>
    <small style='color: #ccc;'>(Kein Heizen, wenn Temp. > Sollwert)</small>
    <br><br>
    <h3>Temperatur:  Dampf / Thermoblock</h3>
    <label for='dampf'>Setpoint (&deg;C):</label>
    <input type='text' id='dampf' name='dampf' value=')rawliteral";
// Nach {DAMPF} bis value='{OFFSET_DAMPF}'
static const char rootChunk_AfterDampf[] PROGMEM = R"rawliteral('>
    <label for='offsetDampf'>Offset (&deg;C):</label>
    <input type='text' id='offsetDampf' name='offsetDampf' value=')rawliteral";
// Nach {OFFSET_DAMPF} bis {BOOST_DAMPF_CHECKED}
static const char rootChunk_AfterOffsetD[] PROGMEM = R"rawliteral('>
    <label for='boostDampf' style='display:inline-block; margin-right:10px;'>
    Boost-Funktion:
    </label>
    <input type='checkbox' id='boostDampf' name='boostDampf' value='1')rawliteral"; // Endet direkt vor dem checked Attribut
// Nach {BOOST_DAMPF_CHECKED} bis {PREVENTHEAT_DAMPF_CHECKED}
static const char rootChunk_AfterBoostD[] PROGMEM = R"rawliteral(>
    <br>
    <small style='color: #ccc;'>(Volle Heizleistung erzwingen bis 75% der Zieltemperatur)</small>
    <br>
    <label for='preventHeatDampf' style='display:inline-block; margin-right:10px;'>
    Heizen oberhalb Setpoint verhindern:
    </label>
    <input type='checkbox' id='preventHeatDampf' name='preventHeatDampf' value='1')rawliteral"; // Endet direkt vor dem checked Attribut
// Nach {PREVENTHEAT_DAMPF_CHECKED} bis value='{KP_WASSER}'
static const char rootChunk_AfterPreventD[] PROGMEM = R"rawliteral(>
    <br>
    <small style='color: #ccc;'>(Kein Heizen, wenn Temp. > Sollwert)</small>
    <br><br>
    <h3>PID-Parameter: Wasser / Kessel</h3>
    <label for='kpWasser'>Kp:</label>
    <input type='text' id='kpWasser' name='kpWasser' value=')rawliteral";
// Nach {KP_WASSER} bis value='{KI_WASSER}'
static const char rootChunk_AfterKpW[] PROGMEM = R"rawliteral('>
    <label for='kiWasser'>Ki:</label>
    <input type='text' id='kiWasser' name='kiWasser' value=')rawliteral";
// Nach {KI_WASSER} bis value='{KD_WASSER}'
static const char rootChunk_AfterKiW[] PROGMEM = R"rawliteral('>
    <label for='kdWasser'>Kd:</label>
    <input type='text' id='kdWasser' name='kdWasser' value=')rawliteral";
// Nach {KD_WASSER} bis value='{WINDOW_SIZE_WASSER}'
static const char rootChunk_AfterKdW[] PROGMEM = R"rawliteral('>
    <label for='windowSizeWasser'>Zeitfenster (ms):</label>
    <input type='text' id='windowSizeWasser' name='windowSizeWasser' value=')rawliteral";
// Nach {WINDOW_SIZE_WASSER} bis value='{KP_DAMPF}'
static const char rootChunk_AfterWindowW[] PROGMEM = R"rawliteral('>
    </br>
    <h3>PID-Parameter: Dampf / Thermoblock</h3>
    <label for='kpDampf'>Kp:</label>
    <input type='text' id='kpDampf' name='kpDampf' value=')rawliteral";
// Nach {KP_DAMPF} bis value='{KI_DAMPF}'
static const char rootChunk_AfterKpD[] PROGMEM = R"rawliteral('>
    <label for='kiDampf'>Ki:</label>
    <input type='text' id='kiDampf' name='kiDampf' value=')rawliteral";
// Nach {KI_DAMPF} bis value='{KD_DAMPF}'
static const char rootChunk_AfterKiD[] PROGMEM = R"rawliteral('>
    <label for='kdDampf'>Kd:</label>
    <input type='text' id='kdDampf' name='kdDampf' value=')rawliteral";
// Nach {KD_DAMPF} bis value='{WINDOW_SIZE_DAMPF}'
static const char rootChunk_AfterKdD[] PROGMEM = R"rawliteral('>
    <label for='windowSizeDampf'>Zeitfenster (ms):</label>
    <input type='text' id='windowSizeDampf' name='windowSizeDampf' value=')rawliteral";
// Nach {WINDOW_SIZE_DAMPF} bis value='{MAX_TEMP_WASSER}'
static const char rootChunk_AfterWindowD[] PROGMEM = R"rawliteral('>
    </br>
    <h3>Sicherheitsabschaltung</h3>
    Die Sicherheitsabschaltung bietet eine softwareseitige &Uuml;bertemperatursicherung,<br>
    ersetzt jedoch keine hardwareseitige L&ouml;sung durch z.B. Bimetall Temperaturschalter!<br>
    Bei der Verwendung von Fast-Heat-Up muss der Wert f&uuml;r die Maximaltemperatur angepasst werden!<br>
    <br>
    <label for='maxTempWasser'>Max. Temp Wasser (&deg;C):</label>
    <input type='text' id='maxTempWasser' name='maxTempWasser' value=')rawliteral";
// Nach {MAX_TEMP_WASSER} bis value='{MAX_TEMP_DAMPF}'
static const char rootChunk_AfterMaxW[] PROGMEM = R"rawliteral('>
    <label for='maxTempDampf'>Max. Temp Dampf (&deg;C):</label>
    <input type='text' id='maxTempDampf' name='maxTempDampf' value=')rawliteral";
// Nach {MAX_TEMP_DAMPF} bis Ende
static const char rootChunk_FormEnd[] PROGMEM = R"rawliteral('>
    <input type='submit' value='Einstellungen speichern'>
  </form>
</body>
</html>
)rawliteral";


// Überarbeitete Funktion handleRoot
void handleRoot()
{
  char buffer[20]; // Puffer für Zahlenumwandlungen (float/int/ulong)

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(rootHtmlHead);
  server.sendContent_P(commonStyle);
  server.sendContent_P(rootHtmlHeadEnd);
  server.sendContent_P(commonNav); // Navigation

  // Body Start
  server.sendContent_P(rootChunk_BodyStart);

  // --- Dynamische Fehlermeldungen generieren und senden ---
  String errorMessageHtml = ""; // Kleiner temporärer String ist ok
  if (wasserSensorError && dampfSensorError) {
    if (!demoModus) {
        errorMessageHtml = "<div class=\"sensor-error-message\">Die Daten der Temperatursensoren sind derzeit nicht verf&uuml;gbar!<br>Bitte Temperatur-Sensoren pr&uuml;fen!</div>";
    } else {
        errorMessageHtml = "<div class=\"sensor-error-message\">Demo-Betrieb: Es sind keine Temperatur-Daten verf&uuml;gbar, bzw. keine Sensoren angeschlossen</div>";
    }
  } else if (wasserSensorError) {
      errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Wasser nicht verf&uuml;gbar</div>";
  } else if (dampfSensorError) {
      errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Dampf nicht verf&uuml;gbar</div>";
  }
  if (errorMessageHtml.length() > 0) {
    server.sendContent(errorMessageHtml); // Fehler als Chunk senden
  }

  // Formular Anfang bis Wasser Setpoint
  server.sendContent_P(rootChunk_FormStart);
  // Wasser Setpoint Wert
  snprintf(buffer, sizeof(buffer), "%d", (int)SetpointWasser);
  server.sendContent(buffer);
  // Nach Wasser Setpoint bis Wasser Offset
  server.sendContent_P(rootChunk_AfterWasser);
  // Wasser Offset Wert
  snprintf(buffer, sizeof(buffer), "%.2f", OffsetWasser);
  server.sendContent(buffer);
  // Nach Wasser Offset bis Boost Wasser Checkbox
  server.sendContent_P(rootChunk_AfterOffsetW);
  // Boost Wasser checked Attribut (falls nötig)
  if (boostWasserActive) server.sendContent(F(" checked"));
  // Nach Boost Wasser bis Prevent Heat Wasser Checkbox
  server.sendContent_P(rootChunk_AfterBoostW);
  // Prevent Heat Wasser checked Attribut (falls nötig)
  if (preventHeatAboveSetpointWasser) server.sendContent(F(" checked"));
  // Nach Prevent Heat Wasser bis Dampf Setpoint
  server.sendContent_P(rootChunk_AfterPreventW);
  // Dampf Setpoint Wert
  snprintf(buffer, sizeof(buffer), "%d", (int)SetpointDampf);
  server.sendContent(buffer);
  // Nach Dampf Setpoint bis Dampf Offset
  server.sendContent_P(rootChunk_AfterDampf);
  // Dampf Offset Wert
  snprintf(buffer, sizeof(buffer), "%.2f", OffsetDampf);
  server.sendContent(buffer);
  // Nach Dampf Offset bis Boost Dampf Checkbox
  server.sendContent_P(rootChunk_AfterOffsetD);
  // Boost Dampf checked Attribut (falls nötig)
  if (boostDampfActive) server.sendContent(F(" checked"));
  // Nach Boost Dampf bis Prevent Heat Dampf Checkbox
  server.sendContent_P(rootChunk_AfterBoostD);
  // Prevent Heat Dampf checked Attribut (falls nötig)
  if (preventHeatAboveSetpointDampf) server.sendContent(F(" checked"));
  // Nach Prevent Heat Dampf bis Kp Wasser
  server.sendContent_P(rootChunk_AfterPreventD);
  // Kp Wasser Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KpWasser);
  server.sendContent(buffer);
  // Nach Kp Wasser bis Ki Wasser
  server.sendContent_P(rootChunk_AfterKpW);
  // Ki Wasser Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KiWasser);
  server.sendContent(buffer);
  // Nach Ki Wasser bis Kd Wasser
  server.sendContent_P(rootChunk_AfterKiW);
  // Kd Wasser Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KdWasser);
  server.sendContent(buffer);
  // Nach Kd Wasser bis Window Size Wasser
  server.sendContent_P(rootChunk_AfterKdW);
  // Window Size Wasser Wert
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeWasser);
  server.sendContent(buffer);
  // Nach Window Size Wasser bis Kp Dampf
  server.sendContent_P(rootChunk_AfterWindowW);
  // Kp Dampf Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KpDampf);
  server.sendContent(buffer);
  // Nach Kp Dampf bis Ki Dampf
  server.sendContent_P(rootChunk_AfterKpD);
  // Ki Dampf Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KiDampf);
  server.sendContent(buffer);
  // Nach Ki Dampf bis Kd Dampf
  server.sendContent_P(rootChunk_AfterKiD);
  // Kd Dampf Wert
  snprintf(buffer, sizeof(buffer), "%.2f", KdDampf);
  server.sendContent(buffer);
  // Nach Kd Dampf bis Window Size Dampf
  server.sendContent_P(rootChunk_AfterKdD);
  // Window Size Dampf Wert
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeDampf);
  server.sendContent(buffer);
  // Nach Window Size Dampf bis Max Temp Wasser
  server.sendContent_P(rootChunk_AfterWindowD);
  // Max Temp Wasser Wert
  snprintf(buffer, sizeof(buffer), "%.2f", maxTempWasser);
  server.sendContent(buffer);
  // Nach Max Temp Wasser bis Max Temp Dampf
  server.sendContent_P(rootChunk_AfterMaxW);
  // Max Temp Dampf Wert
  snprintf(buffer, sizeof(buffer), "%.2f", maxTempDampf);
  server.sendContent(buffer);
  // Formular Ende und HTML Ende
  server.sendContent_P(rootChunk_FormEnd);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

/************************************************************************************
 * Schreibt die Werte in den EEPROM (PID/Eco) - Unverändert
 ************************************************************************************/

void handleSettingsUpdate()
{
  bool valueChanged = false; // Flag um zu prüfen, ob EEPROM.commit() nötig ist

  // --- Bestehende Werte lesen ---
  // Sicherheitslimits
  if (server.hasArg(F("maxTempWasser"))) {
      float newVal = server.arg(F("maxTempWasser")).toFloat();
      if (newVal != maxTempWasser) { maxTempWasser = newVal; EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); valueChanged = true; }
  }
  if (server.hasArg(F("maxTempDampf"))) {
      float newVal = server.arg(F("maxTempDampf")).toFloat();
      if (newVal != maxTempDampf) { maxTempDampf = newVal; EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); valueChanged = true; }
  }
  // Setpoints
   if (server.hasArg(F("wasser"))) {
      float newVal = server.arg(F("wasser")).toFloat();
      if (newVal != SetpointWasser) { SetpointWasser = newVal; EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); valueChanged = true; }
   }
   if (server.hasArg(F("dampf"))) {
      float newVal = server.arg(F("dampf")).toFloat();
      if (newVal != SetpointDampf) { SetpointDampf = newVal; EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); valueChanged = true; }
   }
   // Offsets
   if (server.hasArg(F("offsetWasser"))) {
      float newVal = server.arg(F("offsetWasser")).toFloat();
      if (newVal != OffsetWasser) { OffsetWasser = newVal; EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); valueChanged = true; }
   }
   if (server.hasArg(F("offsetDampf"))) {
       float newVal = server.arg(F("offsetDampf")).toFloat();
       if (newVal != OffsetDampf) { OffsetDampf = newVal; EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); valueChanged = true; }
   }
   // PID Wasser
   if (server.hasArg(F("kpWasser"))) {
       float newVal = server.arg(F("kpWasser")).toFloat();
       if (newVal != KpWasser) { KpWasser = newVal; EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); valueChanged = true; }
   }
   if (server.hasArg(F("kiWasser"))) {
      float newVal = server.arg(F("kiWasser")).toFloat();
      if (newVal != KiWasser) { KiWasser = newVal; EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser); valueChanged = true; }
   }
   if (server.hasArg(F("kdWasser"))) {
       float newVal = server.arg(F("kdWasser")).toFloat();
       if (newVal != KdWasser) { KdWasser = newVal; EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser); valueChanged = true; }
   }
  // PID Dampf
   if (server.hasArg(F("kpDampf"))) {
      float newVal = server.arg(F("kpDampf")).toFloat();
      if (newVal != KpDampf) { KpDampf = newVal; EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf); valueChanged = true; }
   }
   if (server.hasArg(F("kiDampf"))) {
       float newVal = server.arg(F("kiDampf")).toFloat();
       if (newVal != KiDampf) { KiDampf = newVal; EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf); valueChanged = true; }
   }
   if (server.hasArg(F("kdDampf"))) {
      float newVal = server.arg(F("kdDampf")).toFloat();
      if (newVal != KdDampf) { KdDampf = newVal; EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf); valueChanged = true; }
   }
   // Boost Flags
   bool newBoostWasser = server.hasArg(F("boostWasser"));
   if (newBoostWasser != boostWasserActive) {
       boostWasserActive = newBoostWasser;
       EEPROM.put(EEPROM_ADDR_BOOST_WASSER_ACTIVE, boostWasserActive);
       valueChanged = true;
   }
    bool newBoostDampf = server.hasArg(F("boostDampf"));
   if (newBoostDampf != boostDampfActive) {
       boostDampfActive = newBoostDampf;
       EEPROM.put(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, boostDampfActive);
       valueChanged = true;
   }

    // Prevent Heat Above Setpoint Flags
    bool newPreventHeatWasser = server.hasArg(F("preventHeatWasser"));
    if (newPreventHeatWasser != preventHeatAboveSetpointWasser) {
        preventHeatAboveSetpointWasser = newPreventHeatWasser;
        EEPROM.put(EEPROM_ADDR_PREVENTHEAT_WASSER, preventHeatAboveSetpointWasser);
        valueChanged = true;
        Serial.printf("PreventHeat Wasser %s\n", preventHeatAboveSetpointWasser ? "aktiviert" : "deaktiviert");
    }
     bool newPreventHeatDampf = server.hasArg(F("preventHeatDampf"));
    if (newPreventHeatDampf != preventHeatAboveSetpointDampf) {
        preventHeatAboveSetpointDampf = newPreventHeatDampf;
        EEPROM.put(EEPROM_ADDR_PREVENTHEAT_DAMPF, preventHeatAboveSetpointDampf);
        valueChanged = true;
        Serial.printf("PreventHeat Dampf %s\n", preventHeatAboveSetpointDampf ? "aktiviert" : "deaktiviert");
    }

  unsigned long newWindowSizeWasser = windowSizeWasser; // Startwert mit aktuellem Wert
  if (server.hasArg(F("windowSizeWasser"))) {
      char* endptr;
      // Konvertiere den String-Parameter zu unsigned long
      unsigned long val = strtoul(server.arg(F("windowSizeWasser")).c_str(), &endptr, 10);
      // Prüfe, ob Konvertierung erfolgreich war und Wert sinnvoll ist (z.B. >0 und nicht zu groß)
      if (*endptr == '\0' && val > 0 && val <= 60000) { // Beispiel: max 60 Sekunden
          newWindowSizeWasser = val;
      } else {
        Serial.printf("WARNUNG: Ungültiger Wert für windowSizeWasser empfangen: %s\n", server.arg(F("windowSizeWasser")).c_str());
      }
  }
  // Wenn sich der Wert geändert hat:
  if (newWindowSizeWasser != windowSizeWasser) {
      windowSizeWasser = newWindowSizeWasser;
      EEPROM.put(EEPROM_ADDR_WINDOWSIZE_WASSER, windowSizeWasser);
      pidWasser.SetOutputLimits(0, windowSizeWasser); // <-- WICHTIG: PID Limit sofort aktualisieren!
      valueChanged = true;
      Serial.printf("Fenstergröße Wasser aktualisiert auf: %lu ms\n", windowSizeWasser);
  }

  unsigned long newWindowSizeDampf = windowSizeDampf; // Startwert mit aktuellem Wert
  if (server.hasArg(F("windowSizeDampf"))) {
       char* endptr;
       unsigned long val = strtoul(server.arg(F("windowSizeDampf")).c_str(), &endptr, 10);
       if (*endptr == '\0' && val > 0 && val <= 60000) {
           newWindowSizeDampf = val;
       } else {
         Serial.printf("WARNUNG: Ungültiger Wert für windowSizeDampf empfangen: %s\n", server.arg(F("windowSizeDampf")).c_str());
       }
  }
   // Wenn sich der Wert geändert hat:
  if (newWindowSizeDampf != windowSizeDampf) {
      windowSizeDampf = newWindowSizeDampf;
      EEPROM.put(EEPROM_ADDR_WINDOWSIZE_DAMPF, windowSizeDampf);
      pidDampf.SetOutputLimits(0, windowSizeDampf); // <-- WICHTIG: PID Limit sofort aktualisieren!
      valueChanged = true;
       Serial.printf("Fenstergröße Dampf aktualisiert auf: %lu ms\n", windowSizeDampf);
  }


  // --- EEPROM speichern und PID aktualisieren ---
  if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
    if (EEPROM.commit()) {
      Serial.println("Einstellungen erfolgreich in EEPROM gespeichert.");
    } else {
      Serial.println("FEHLER: EEPROM Commit fehlgeschlagen!");
    }
  }

  // PID-Parameter an die Regler übergeben
  pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
  pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);

  // Zurück zur Hauptseite (bereits vorhanden)
  server.sendHeader(F("Location"), F("/"));
  server.send(303);
}

/************************************************************************************
 * Handler für die Firmware & Einstellungen Seite - Heap-Optimiert
 ************************************************************************************/

// --- PROGMEM Chunks für handleFirmware ---
static const char firmwareHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Firmware & Einstellungen</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
)rawliteral"; // Head inklusive </head>

static const char firmwareBodyStart[] PROGMEM = R"rawliteral(
<body>
  <h1>Firmware & Einstellungen</h1>
  <form>
    <h3>Firmware-Info</h3>
    Firmware-Version: )rawliteral"; // Endet vor {FIRMWAREVERSION}
static const char firmwareInfoChunk2[] PROGMEM = R"rawliteral(<br>
    Hersteller: )rawliteral"; // Endet vor {SWHERSTELLER}
static const char firmwareInfoChunk3[] PROGMEM = R"rawliteral(<br>
    E-Mail:  )rawliteral"; // Endet vor {SWHERSTELLERMAIL}
static const char firmwareInfoChunk4[] PROGMEM = R"rawliteral(<br>
    Website:  )rawliteral"; // Endet vor {SWHERSTELLERWEBSITE}
static const char firmwareInfoChunkEnd[] PROGMEM = R"rawliteral(<br>
  </form>
)rawliteral"; // Ende Firmware-Info Sektion

static const char firmwareUpdateSection[] PROGMEM = R"rawliteral(
  <form method='POST' action='/update' enctype='multipart/form-data'>
    <h3>Firmware-Update</h3>
    Das Firmware-Update kann durch den Upload einer .bin-Datei durchgef&uuml;hrt werden.<br>
    Nach dem Update wird ein automatischer Neustart durchgef&uuml;hrt.<br>
    Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.<br>
    <br>
    <input type='file' name='firmware' accept='.bin'>
    <br>
    <button>Update starten</button>
  </form>
)rawliteral";

static const char firmwareFileManagerSection[] PROGMEM = R"rawliteral(
  <form method='GET' action='/Dateimanager' target='_blank'enctype='multipart/form-data'>
    <h3>Dateimanager</h3>
    Verwaltung von Dateien des Systems.<br>
    <button type='submit' >Dateimanager</button>
  </form>
)rawliteral";

static const char firmwareExportSection[] PROGMEM = R"rawliteral(
  <form method='GET' action='/exportSettings' enctype='multipart/form-data'>
    <h3>Einstellungen exportieren</h3>
    Sicherung aller aktuellen Einstellungen in einer Datei.<br>
    <button type='submit' >Exportieren</button>
  </form>
)rawliteral";

static const char firmwareImportSection[] PROGMEM = R"rawliteral(
  <form method='POST' action='/importSettings' enctype='multipart/form-data'>
    <h3>Einstellungen importieren</h3>
    Alle Einstellungen aus einer zuvor exportierten .bin-Datei wiederherstellen.<br><br>
    <b style='color: #FFCC00;'>ACHTUNG:</b><br>
    Dieser Vorgang &uuml;berschreibt ALLE aktuellen Einstellungen!<br>
    Nach dem Import einfolgt ein automatischer Neustart.<br>
    <br>
    <input type='file' name='settings' accept='.bin' required>
    <br>
    <button>Import & Neustart</button>
  </form>
)rawliteral";

static const char firmwareResetSection[] PROGMEM = R"rawliteral(
  <form action='/resetDefaults' method='POST' onsubmit='return confirmResetDefaults();'>
    <h3>Werkseinstellungen</h3>
    Setzt ALLE Einstellungen (mit Ausnahme der WiFi-Konfiguration, der Shot- und Betriebsstundenz&auml;hler) auf die Standardwerte zur&uuml;ck.<br><br>
    <b style='color: #FFCC00;'>ACHTUNG:</b><br>
    Es ist zu empfehlen, vorher einen Export der Einstellungen durchzuf&uuml;hren.
    <br>
    <button type='submit' class='danger'>Werkseinstellungen laden</button>
  </form>
)rawliteral";

static const char firmwareBodyEnd[] PROGMEM = R"rawliteral(
</body>
</html>
)rawliteral";


// --- Neue Funktion handleFirmware ---
void handleFirmware() { // URL: /Firmware, Methode: GET

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(firmwareHtmlHead); // Enthält öffnendes <html> und <head> bis </head>
  server.sendContent_P(commonStyle);
  // </head> ist im Head-Chunk enthalten
  server.sendContent_P(commonNav); // Navigation

  // Body Start und Firmware Info Sektion
  server.sendContent_P(firmwareBodyStart); // Enthält <body>, <h1> und Start der <form> für Info
  // Firmware Version
  if (version.length() > 0) { // Prüfen, ob String nicht leer ist
    server.sendContent(version);
  }
  server.sendContent_P(firmwareInfoChunk2); // Enthält <br>Hersteller:
  // Hersteller
  if (versionHersteller.length() > 0) {
    server.sendContent(versionHersteller);
  }
  server.sendContent_P(firmwareInfoChunk3); // Enthält <br>E-Mail:
  // Mail (ist HTML, kann direkt gesendet werden)
  if (versionHerstellerMail.length() > 0) {
    server.sendContent(versionHerstellerMail);
  }
  server.sendContent_P(firmwareInfoChunk4); // Enthält <br>Website:
  // Website (ist HTML, kann direkt gesendet werden)
   if (versionHerstellerWeb.length() > 0) {
    server.sendContent(versionHerstellerWeb);
  }
  server.sendContent_P(firmwareInfoChunkEnd); // Enthält <br></form>

  // Firmware Update Sektion
  server.sendContent_P(firmwareUpdateSection);

  // Dateimanager Sektion
  server.sendContent_P(firmwareFileManagerSection);

  // Einstellungen Export Sektion
  server.sendContent_P(firmwareExportSection);

  // Einstellungen Import Sektion
  server.sendContent_P(firmwareImportSection);

  // Werkseinstellungen Sektion
  server.sendContent_P(firmwareResetSection);

  // Body / HTML Ende
  server.sendContent_P(firmwareBodyEnd);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

/************************************************************************************
 * Handler für AutTune Wasser
 ************************************************************************************/
// --- PROGMEM Chunks für handleAutoTuneWasser ---
static const char autotuneWasserHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>AutoTune Wasser</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
)rawliteral";

static const char autotuneWasserBodyStart[] PROGMEM = R"rawliteral(
<body>
  <h1>PID-Tuning</h1>
)rawliteral";

static const char autotuneWasserStartSuccess[] PROGMEM = R"rawliteral(
  <form action='/PID-Tuning-Abbruch' method='POST' >
    <p>AutoTune gestartet: Wasser-PID</p>
    <br>
    <input type='submit' value='PID-Tuning abbrechen'>
  </form>
)rawliteral";

static const char autotuneWasserAlreadyActive[] PROGMEM = R"rawliteral(
  <form action='/PID-Tuning-Abbruch' method='POST' >
    <p style="color: red;">AutoTune ist bereits aktiv!</p>
    <br>
    <input type='submit' value='PID-Tuning abbrechen'>
  </form>
)rawliteral";

static const char autotuneWasserBodyEnd[] PROGMEM = R"rawliteral(
</body>
</html>
)rawliteral";

// --- Neue Funktion handleAutoTuneWasser ---
void handleAutoTuneWasser() {
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  server.sendContent_P(autotuneWasserHtmlHead);
  server.sendContent_P(commonStyle);
  // </head> ist im Head-Chunk enthalten
  server.sendContent_P(commonNav);
  server.sendContent_P(autotuneWasserBodyStart);

  if (!autoTuneWasserActive && !autoTuneDampfActive) {
    startAutoTuneWasser(); // Funktion zum Starten des Tunings aufrufen
    server.sendContent_P(autotuneWasserStartSuccess);
  } else {
    server.sendContent_P(autotuneWasserAlreadyActive);
  }

  server.sendContent_P(autotuneWasserBodyEnd);
  server.sendContent(F("")); // Abschluss-Chunk
}

/************************************************************************************
 * Handler für AutTune Dampf
 ************************************************************************************/

// --- PROGMEM Chunks für handleAutoTuneDampf ---
static const char autotuneDampfHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>AutoTune Dampf</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
)rawliteral";

// BodyStart, AlreadyActive und BodyEnd können von handleAutoTuneWasser wiederverwendet werden
// (autotuneWasserBodyStart, autotuneWasserAlreadyActive, autotuneWasserBodyEnd)

static const char autotuneDampfStartSuccess[] PROGMEM = R"rawliteral(
  <form action='/PID-Tuning-Abbruch' method='POST' >
    <p>AutoTune gestartet: Dampf-PID</p>
    <br>
    <input type='submit' value='PID-Tuning abbrechen'>
  </form>
)rawliteral";

// --- Neue Funktion handleAutoTuneDampf ---
void handleAutoTuneDampf() {
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  server.sendContent_P(autotuneDampfHtmlHead); // Eigener Head-Chunk
  server.sendContent_P(commonStyle);
  // </head> ist im Head-Chunk enthalten
  server.sendContent_P(commonNav);
  server.sendContent_P(autotuneWasserBodyStart); // Wiederverwendet

  if (!autoTuneWasserActive && !autoTuneDampfActive) {
    startAutoTuneDampf(); // Funktion zum Starten des Tunings aufrufen
    server.sendContent_P(autotuneDampfStartSuccess); // Eigener Success-Chunk
  } else {
    server.sendContent_P(autotuneWasserAlreadyActive); // Wiederverwendet
  }

  server.sendContent_P(autotuneWasserBodyEnd); // Wiederverwendet
  server.sendContent(F("")); // Abschluss-Chunk
}

/************************************************************************************
 * Handler zum Speichern der AutoTune-Einstellungen - ausgelagert
 ************************************************************************************/
void handleUpdateAutotuneSettings() {
  bool changed = false; // Flag, um zu prüfen, ob EEPROM.commit() nötig ist

  // --- Wasser Parameter ---
  if (server.hasArg(F("tuningStepWasser")))
  {
    tuningStepWasser = server.arg(F("tuningStepWasser")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser);
    changed = true;
  }
  if (server.hasArg(F("tuningNoiseWasser")))
  {
    tuningNoiseWasser = server.arg(F("tuningNoiseWasser")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser);
    changed = true;
  }
  if (server.hasArg(F("tuningStartValueWasser")))
  {
    tuningStartValueWasser = server.arg(F("tuningStartValueWasser")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser);
    changed = true;
  }
  if (server.hasArg(F("tuningLookBackWasser")))
  {
    // Wichtig: toInt() verwenden, da es unsigned int ist, aber über HTTP als String kommt
    int lookbackInt = server.arg(F("tuningLookBackWasser")).toInt();
    if (lookbackInt >= 0) { // Grundlegende Plausibilitätsprüfung
        tuningLookBackWasser = (unsigned int)lookbackInt;
        EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser);
        changed = true;
    }
  }

  // --- Dampf Parameter ---
  if (server.hasArg(F("tuningStepDampf")))
  {
    tuningStepDampf = server.arg(F("tuningStepDampf")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf);
    changed = true;
  }
  if (server.hasArg(F("tuningNoiseDampf")))
  {
    tuningNoiseDampf = server.arg(F("tuningNoiseDampf")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf);
    changed = true;
  }
  if (server.hasArg(F("tuningStartValueDampf")))
  {
    tuningStartValueDampf = server.arg(F("tuningStartValueDampf")).toFloat();
    EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf);
    changed = true;
  }
  if (server.hasArg(F("tuningLookBackDampf")))
  {
     // Wichtig: toInt() verwenden, da es unsigned int ist, aber über HTTP als String kommt
    int lookbackInt = server.arg(F("tuningLookBackDampf")).toInt();
     if (lookbackInt >= 0) { // Grundlegende Plausibilitätsprüfung
        tuningLookBackDampf = (unsigned int)lookbackInt;
        EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf);
        changed = true;
    }
  }

  // Nur speichern, wenn sich etwas geändert hat
  if (changed) {
      if(!EEPROM.commit()){
         Serial.println("FEHLER: EEPROM Commit für AutoTune-Settings fehlgeschlagen!");
      } else {
         Serial.println("AutoTune-Parameter gespeichert.");
      }
  }

  // Weiterleitung zurück zur PID-Tuning Seite
  server.sendHeader(F("Location"), F("/PID-Tuning")); // URL bleibt gleich
  server.send(303); // 303 See Other - Korrekt für Redirect nach POST
}

/************************************************************************************
 * Handler für PID-Tuning
 ************************************************************************************/

// --- PROGMEM Chunks für handlePidTuning ---
static const char pidTuningPageHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>PID-Tuning</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
)rawliteral";

static const char pidTuningPageBodyStart[] PROGMEM = R"rawliteral(
<body>
)rawliteral";

static const char pidTuningPageAbortForm[] PROGMEM = R"rawliteral(
  <form action='/PID-Tuning-Abbruch' method='POST' >
    <h3>Laufendes PID-Tuning abbrechen?</h3>
    <input type='submit' value='Abbrechen'>
  </form></br>
)rawliteral";

static const char pidTuningPageIntro[] PROGMEM = R"rawliteral(
  <h1>PID-Tuning</h1>
  <form>Das automatische PID-Tuning dient dazu, die optimalen Parameter f&uuml;r den PID-Regler (Proportional-, Integral- und Differentialanteil) selbst&auml;ndig zu ermitteln. Dabei analysiert das System mithilfe von Algorithmen das Regelverhalten (z. B. Reaktion auf einen Testimpuls) und passt Kp, Ki und Kd so an, dass gew&uuml;nschte Kriterien wie kurze Einschwingzeit, geringe &Uuml;berschwingung und stabile Regelung erreicht werden
  <br><br>
  Ein AutoTune-Vorgang kann durchaus bis zu 60 Minuten in Anspruch nehmen!
  <br><br>
  <b style='color: #FFCC00;'>WICHTIG:</b><br>
  Vor dem Start des PID-Tunings sollte die Maschine die gew&uuml;nschte Betriebstemperatur erreicht haben und stabil sein.<br>
  Andernfalls k&ouml;nnten die automatisch ermittelten PID-Werte f&uuml;r diesen Temperaturbereich ungenau oder nicht optimal sein.
  <br><br>
  </form>
  <form action='/AutoTune-Wasser' method='POST' >
    <h3>Automatisches PID-Tuning f&uuml;r Wasser</h3>
    Automatische Ermittlung der PID-Werte f&uuml;r Wasser / Kessel<br>
    <input type='submit' value='Tuning starten'>
  </form>
  <form action='/AutoTune-Dampf' method='POST' >
    <h3>Automatisches PID-Tuning f&uuml;r Dampf</h3>
    Automatische Ermittlung der PID-Werte f&uuml;r Dampf / Thermoblock<br>
    <input type='submit' value='Tuning starten'>
  </form>
)rawliteral";

// --- Chunks für das Parameter-Formular ---
static const char pidTuningPageParamsFormStart[] PROGMEM = R"rawliteral(
  <form action='/updateAutotuneSettings' method='POST'>
    <h3>AutoTune-Parameter (Zeitproportional)</h3>
    Hier k&ouml;nnen die AutoTune-Parameter ge&auml;ndert werden.<br>&Auml;nderungen sollten mit Vorsicht durchgef&uuml;hrt werden!<br><br>
    <h3>Parameter Wasser / Kessel</h3>
    <label for='tuningStepWasser'>Step (Output 0-)rawliteral"; // Endet vor {WINDOW_SIZE_WASSER}
static const char pidTuningPageParamsW1[] PROGMEM = R"rawliteral():</label>
    <input type='text' id='tuningStepWasser' name='tuningStepWasser' value=')rawliteral"; // Endet vor {TUNING_STEP_WASSER}
static const char pidTuningPageParamsW2[] PROGMEM = R"rawliteral('>
    <label for='tuningNoiseWasser'>Noise (&deg;C):</label>
    <input type='text' id='tuningNoiseWasser' name='tuningNoiseWasser' value=')rawliteral"; // Endet vor {TUNING_NOISE_WASSER}
static const char pidTuningPageParamsW3[] PROGMEM = R"rawliteral('>
    <label for='tuningStartValueWasser'>StartValue (Output 0-)rawliteral"; // Endet vor {WINDOW_SIZE_WASSER}
static const char pidTuningPageParamsW4[] PROGMEM = R"rawliteral():</label>
    <input type='text' id='tuningStartValueWasser' name='tuningStartValueWasser' value=')rawliteral"; // Endet vor {TUNING_STARTVALUE_WASSER}
static const char pidTuningPageParamsW5[] PROGMEM = R"rawliteral('>
    <label for='tuningLookBackWasser'>LookBack (Sekunden):</label>
    <input type='text' id='tuningLookBackWasser' name='tuningLookBackWasser' value=')rawliteral"; // Endet vor {TUNING_LOOKBACK_WASSER}
static const char pidTuningPageParamsDStart[] PROGMEM = R"rawliteral('>
    <br><h3>Parameter Dampf / Thermoblock</h3>
    <label for='tuningStepDampf'>Step (Output 0-)rawliteral"; // Endet vor {WINDOW_SIZE_DAMPF}
static const char pidTuningPageParamsD1[] PROGMEM = R"rawliteral():</label>
    <input type='text' id='tuningStepDampf' name='tuningStepDampf' value=')rawliteral"; // Endet vor {TUNING_STEP_DAMPF}
static const char pidTuningPageParamsD2[] PROGMEM = R"rawliteral('>
    <label for='tuningNoiseDampf'>Noise (&deg;C):</label>
    <input type='text' id='tuningNoiseDampf' name='tuningNoiseDampf' value=')rawliteral"; // Endet vor {TUNING_NOISE_DAMPF}
static const char pidTuningPageParamsD3[] PROGMEM = R"rawliteral('>
    <label for='tuningStartValueDampf'>StartValue (Output 0-)rawliteral"; // Endet vor {WINDOW_SIZE_DAMPF}
static const char pidTuningPageParamsD4[] PROGMEM = R"rawliteral():</label>
    <input type='text' id='tuningStartValueDampf' name='tuningStartValueDampf' value=')rawliteral"; // Endet vor {TUNING_STARTVALUE_DAMPF}
static const char pidTuningPageParamsD5[] PROGMEM = R"rawliteral('>
    <label for='tuningLookBackDampf'>LookBack (Sekunden):</label>
    <input type='text' id='tuningLookBackDampf' name='tuningLookBackDampf' value=')rawliteral"; // Endet vor {TUNING_LOOKBACK_DAMPF}
static const char pidTuningPageParamsFormEnd[] PROGMEM = R"rawliteral('>
    <input type='submit' value='Parameter speichern'>
  </form>
)rawliteral";

static const char pidTuningPageExplanation[] PROGMEM = R"rawliteral(
  <br><hr><br>
  <form>
    <h3>Erkl&auml;rung der Parameter (Zeitproportional):</h3>
    <p><strong>Noise (&deg;C):</strong><br>
    Beobachte das \"Zittern\" der Temperaturanzeige, wenn die Temperatur stabil (nahe am Zielwert orientiert) ist.<br>Wenn sie z.B. um +/- 0.5&deg;C schwankt, setze Noise auf <code>1.0</code>.<br>Dieser Wert definiert ein Toleranzband, um das Sensorrauschen zu ignorieren.</p>
    <p><strong>StartValue (ms):</strong><br>
    Sch&auml;tze, wie viel Heizzeit (in Millisekunden, innerhalb des eingestellten Zeitfensters (siehe PID-Einstellung) n&ouml;tig ist, um die Zieltemperatur konstant zu halten.<br>Dies ist die durchschnittliche Heizzeit im stabilen Zustand (z.B. <code>250</code> ms).</p>
    <p><strong>Step (ms):</strong><br>
    Die Gr&ouml;&szlig;e des \"Sprungs\" der Heizzeit (in ms) nach oben/unten um den <code>StartValue</code>, den AutoTune nutzt, um die Temperatur zum Schwingen zu zwingen.<br>Muss gro&szlig; genug f&uuml;r eine Reaktion sein, aber nicht 0-100% (z.B. <code>500</code> ms).</p>
    <p><strong>LookBack (s):</strong><br>
    AutoTune erzeugt eine *langsame* Welle der Temperatur. Miss (in einem Testlauf) oder sch&auml;tze, wie lange eine volle Welle dauert (Spitze zu Spitze, in Sekunden).<br>Setze Lookback auf einen Wert, der *deutlich l&auml;nger* ist als diese Dauer (z.B. 1.5x so lang; Welle=130s => Lookback=<code>180</code>s).<br>Dieser Wert muss *vor* dem Start festgelegt werden und sagt AutoTune, wie weit es zur&uuml;ckschauen soll, um die Welle zu messen.</p>
    <p><strong>Kontrolle per Chart:</strong><br>
    W&auml;hrend AutoTune l&auml;uft, beobachte das Temperaturchart.<br>Wenn sich dort eine langsame, regelm&auml;&szlig;ige Welle (wie ein \"langgezogener Sinus\") um den Sollwert entwickelt, ist das ein gutes Zeichen, dass der Prozess funktioniert.</p>
  </form>
</body>
</html>
)rawliteral";

// --- handlePidTuning ---
void handlePidTuning() {
  char buffer[20]; // Puffer für Zahlenumwandlungen

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  server.sendContent_P(pidTuningPageHtmlHead);
  server.sendContent_P(commonStyle);
  // </head> ist im Head-Chunk enthalten
  server.sendContent_P(commonNav);
  server.sendContent_P(pidTuningPageBodyStart);

  // Optionales "Abbrechen"-Formular anzeigen
  if (autoTuneWasserActive || autoTuneDampfActive) {
    server.sendContent_P(pidTuningPageAbortForm);
  }

  // Einführungstext und Start-Buttons
  server.sendContent_P(pidTuningPageIntro);

  // Parameter-Formular (Stück für Stück)
  server.sendContent_P(pidTuningPageParamsFormStart); // Bis vor WindowSize Wasser
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeWasser); server.sendContent(buffer); // WindowSize Wasser
  server.sendContent_P(pidTuningPageParamsW1); // Bis vor Step Wasser
  snprintf(buffer, sizeof(buffer), "%.2f", tuningStepWasser); server.sendContent(buffer); // Step Wasser
  server.sendContent_P(pidTuningPageParamsW2); // Bis vor Noise Wasser
  snprintf(buffer, sizeof(buffer), "%.2f", tuningNoiseWasser); server.sendContent(buffer); // Noise Wasser
  server.sendContent_P(pidTuningPageParamsW3); // Bis vor WindowSize Wasser (für StartValue)
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeWasser); server.sendContent(buffer); // WindowSize Wasser
  server.sendContent_P(pidTuningPageParamsW4); // Bis vor StartValue Wasser
  snprintf(buffer, sizeof(buffer), "%.2f", tuningStartValueWasser); server.sendContent(buffer); // StartValue Wasser
  server.sendContent_P(pidTuningPageParamsW5); // Bis vor LookBack Wasser
  snprintf(buffer, sizeof(buffer), "%u", tuningLookBackWasser); server.sendContent(buffer); // LookBack Wasser

  server.sendContent_P(pidTuningPageParamsDStart); // Bis vor WindowSize Dampf
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeDampf); server.sendContent(buffer); // WindowSize Dampf
  server.sendContent_P(pidTuningPageParamsD1); // Bis vor Step Dampf
  snprintf(buffer, sizeof(buffer), "%.2f", tuningStepDampf); server.sendContent(buffer); // Step Dampf
  server.sendContent_P(pidTuningPageParamsD2); // Bis vor Noise Dampf
  snprintf(buffer, sizeof(buffer), "%.2f", tuningNoiseDampf); server.sendContent(buffer); // Noise Dampf
  server.sendContent_P(pidTuningPageParamsD3); // Bis vor WindowSize Dampf (für StartValue)
  snprintf(buffer, sizeof(buffer), "%lu", windowSizeDampf); server.sendContent(buffer); // WindowSize Dampf
  server.sendContent_P(pidTuningPageParamsD4); // Bis vor StartValue Dampf
  snprintf(buffer, sizeof(buffer), "%.2f", tuningStartValueDampf); server.sendContent(buffer); // StartValue Dampf
  server.sendContent_P(pidTuningPageParamsD5); // Bis vor LookBack Dampf
  snprintf(buffer, sizeof(buffer), "%u", tuningLookBackDampf); server.sendContent(buffer); // LookBack Dampf

  server.sendContent_P(pidTuningPageParamsFormEnd); // Rest des Formulars

  // Erklärungen
  server.sendContent_P(pidTuningPageExplanation);

  server.sendContent(F("")); // Abschluss-Chunk
}

/************************************************************************************
 * Handler für die Antwort nach Abbruch des PID-Tunings - Heap-Optimiert
 ************************************************************************************/

// PROGMEM Chunks für die Seite (Namen angepasst für Klarheit)
static const char abortPidTuningHtmlStart[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Info</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char abortPidTuningHtmlHeadEnd[] PROGMEM = R"rawliteral(</head>)rawliteral";
static const char abortPidTuningHtmlBody[] PROGMEM = R"rawliteral(
<body>
  <h1>PID-Tuning Abbruch</h1>
  <form>
    <h3>Das PID-Tuning wurde erfolgreich abgebrochen.</h3>
    <p>Der Normalbetrieb wird nun fortgesetzt.</p>
  </form>
</body>
</html>
)rawliteral";

// --- Neue Funktion handlePidTuningAbbruch ---
void handlePidTuningAbbruch() {
    // Seite senden
    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    server.send(200, F("text/html"), "");

    server.sendContent_P(abortPidTuningHtmlStart);
    server.sendContent_P(commonStyle);
    server.sendContent_P(abortPidTuningHtmlHeadEnd);
    server.sendContent_P(commonNav);
    server.sendContent_P(abortPidTuningHtmlBody); // Body ist komplett statisch
    server.sendContent(F("")); // Abschluss-Chunk

    // Eigentliche Abbruch-Logik ausführen, NACHDEM die Antwort gesendet wurde
    stopAutoTuneWasser();
    stopAutoTuneDampf();
}

/************************************************************************************
 * Speichert Eco-Einstellungen - URL angepasst
 ************************************************************************************/

void handleEcoUpdate()
{
  if (server.hasArg(F("ecoMode")))            ecoModeMinutes     = server.arg(F("ecoMode")).toInt();
  if (server.hasArg(F("ecoModeTempWasser")))  ecoModeTempWasser  = server.arg(F("ecoModeTempWasser")).toInt();
  if (server.hasArg(F("ecoModeTempDampf")))   ecoModeTempDampf   = server.arg(F("ecoModeTempDampf")).toInt();

  if (server.hasArg(F("dynamicEcoMode")))
  {
    dynamicEcoActive = true;
  }
  else
  {
    dynamicEcoActive = false;
  }

  if (server.hasArg(F("dampfDelay")))
  {
    dampfVerzoegerung = server.arg(F("dampfDelay")).toInt();
  }

  EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES,       ecoModeMinutes);
  EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER,   ecoModeTempWasser);
  EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF,    ecoModeTempDampf);
  EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE,      dynamicEcoActive);
  EEPROM.put(EEPROM_ADDR_STEAM_DELAY,           dampfVerzoegerung);
  EEPROM.commit();

  // Wenn Eco deaktiviert, Setpoints zurückladen
  if (ecoModeMinutes == 0)
  {
    ecoModeAktiv         = false;
    ecoModeActivatedTime = 0;
    EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
    EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
  }

  server.sendHeader(F("Location"), F("/ECO")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Speichert Geräte-Infos 
 ************************************************************************************/

void handleInfoUpdate()
{
  bool changed = false;
  if (server.hasArg(F("hersteller")))
  {
    strncpy(infoHersteller, server.arg(F("hersteller")).c_str(), sizeof(infoHersteller)-1);
    infoHersteller[sizeof(infoHersteller)-1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
    changed = true;
  }
  if (server.hasArg(F("modell")))
  {
    strncpy(infoModell, server.arg(F("modell")).c_str(), sizeof(infoModell)-1);
    infoModell[sizeof(infoModell)-1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
    changed = true;
  }
  if (server.hasArg(F("zusatz")))
  {
    strncpy(infoZusatz, server.arg(F("zusatz")).c_str(), sizeof(infoZusatz)-1);
    infoZusatz[sizeof(infoZusatz)-1] = '\0';
    EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
    changed = true;
  }

  if (changed) {
      EEPROM.commit();
  }

  server.sendHeader(F("Location"), F("/Info")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Speichert das Wartungsintervall 
 ************************************************************************************/

void handleMaintenanceUpdate()
{
  bool changed = false;
  // Wartungsintervall speichern
  if (server.hasArg(F("maintenanceInterval")))
  {
      int newInterval = server.arg(F("maintenanceInterval")).toInt();
      if (newInterval < 0) newInterval = 0; // Sicherstellen, dass nicht negativ
      if (newInterval != maintenanceInterval) {
          maintenanceInterval = newInterval;
          EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL, maintenanceInterval);
          changed = true;
          Serial.printf("Wartungsintervall gesetzt auf: %d Shots\n", maintenanceInterval);
      }
  }

  if (changed) {
      EEPROM.commit();
  }

  server.sendHeader(F("Location"), F("/Info")); // URL angepasst
  server.send(303);
}

/************************************************************************************
 * Handler für JSON-Chart-Daten (/Chart-Daten) - Heap-Optimiert
 ************************************************************************************/
void handleChartData() {
    // JSON-Daten als Chunks senden
    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    // WICHTIG: Content-Type beibehalten!
    server.send(200, F("application/json"), ""); // Header senden

    char buffer[20]; // Puffer für Zahlen (double mit einer Nachkommastelle)

    server.sendContent(F("{\"wasser\":"));
    // Formatierung mit einer Nachkommastelle, wie im Chart-Tooltip
    snprintf(buffer, sizeof(buffer), "%.1f", InputWasser);
    server.sendContent(buffer);

    server.sendContent(F(",\"setpointwasser\":"));
    snprintf(buffer, sizeof(buffer), "%.1f", SetpointWasser);
    server.sendContent(buffer);

    server.sendContent(F(",\"dampf\":"));
    snprintf(buffer, sizeof(buffer), "%.1f", InputDampf);
    server.sendContent(buffer);

    server.sendContent(F(",\"setpointdampf\":"));
    snprintf(buffer, sizeof(buffer), "%.1f", SetpointDampf);
    server.sendContent(buffer);

    server.sendContent(F("}"));
    server.sendContent(F("")); // Abschluss-Chunk
}

/************************************************************************************
 * Handler für Temperatur-Charts (Live) - URL angepasst
 * Lädt chart.js bevorzugt aus LittleFS, sonst CDN, sonst Fehler.
 ************************************************************************************/
void handleCharts() // URL: /Chart
{
  bool chartJsLocal = LittleFS.exists("/chart.umd.min.js");
  bool isConnected = (WiFi.status() == WL_CONNECTED);
  String chartJsUrl = "";
  bool canLoadChartJs = false;

  if (chartJsLocal) {
    chartJsUrl = "/chart.umd.min.js"; // Lokale Datei verwenden
    canLoadChartJs = true;
    Serial.println("Chart-Handler: Verwende lokale chart.umd.min.js");
  } else if (isConnected) {
    chartJsUrl = "https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"; // Fallback auf CDN
    canLoadChartJs = true;
    Serial.println("Chart-Handler: Lade chart.umd.min.js vom CDN (lokal nicht gefunden).");
  } else {
    // Weder lokal noch Internet -> Chart kann nicht geladen werden
    canLoadChartJs = false;
    Serial.println("Chart-Handler: Kann chart.js nicht laden (nicht lokal, keine Verbindung).");
  }

  // --- HTML-Teile definieren (PROGMEM für Effizienz) ---

  // Teil 1: HTML-Start, Meta-Tags, spezifische Chart-Styles
  static const char chartsHtml_HeadStart[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
  <title>Live-Temperaturverlauf</title>
  <meta charset="UTF-8">
  <meta name='theme-color' content='#111111'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral"; // Script-Tag kommt dynamisch

  // Teil 1b: Spezifische Styles (wie vorher)
  static const char chartsHtml_SpecificStyles[] PROGMEM = R"rawliteral(
  <style>
    /* *** Body und Haupt-Wrapper für Flexbox *** */
    html { height: 100%; } /* Wichtig für 100vh */
    body {
    margin: 0; /* Standard-Margin entfernen */
    display: flex;
    flex-direction: column; /* Stapelt Navigation und main-content */
    min-height: 100vh; /* Mindestens die Höhe des Viewports */
      /* Ggf. allgemeine Body-Styles wie Hintergrund hierher verschieben,
         falls sie nicht in commonStyle sind */
    }
    /* Optional: Wenn die Navigation eine fixe Höhe hat */
    /* .your-nav-class { flex-shrink: 0; } */

    main.chart-page-content { /* *** NEUER Wrapper *** */
      flex: 1; /* Nimmt den Restplatz nach der Navigation ein */
      display: flex;
      flex-direction: column; /* Stapelt Titel, Selektor, Chart usw. */
      padding: 15px; /* Außenabstand für den Inhalt */
      box-sizing: border-box;
      overflow: hidden; /* Verhindert, dass Inhalt überläuft */
      max-width: 1600px; /* Optionale Maximalbreite für sehr breite Bildschirme */
      margin-left: auto;  /* Zentriert den Inhalt horizontal */
      margin-right: auto; /* Zentriert den Inhalt horizontal */
      width: 100%;       /* Nimmt volle Breite bis max-width ein */
    }
    /* Spezifische Styles für die Chart-Seite (Ergänzung zu commonStyle) */
    h1.chart-title { /* Eigene Klasse um Konflikte zu vermeiden */
      text-align: center; color: #efefef; margin-top: 20px; margin-bottom: 10px;
      font-weight: 300; font-size: 1.8rem;
    }
    .chart-container {
      width: 95%; height: 65vh; min-height: 350px;
      margin: 15px auto; background: rgba(0, 0, 0, 0.2);
      box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); border-radius: 15px;
      position: relative; border: 1px solid rgba(255, 255, 255, 0.1);
      padding: 20px; box-sizing: border-box;
    }
    .chart-container canvas { width: 100% !important; height: 100% !important; }
    .update-rate-selector {
      margin: 10px auto 20px auto; display: flex; justify-content: center;
      align-items: center; gap: 12px;
    }
    .update-rate-selector label { color: #ccc; font-size: 0.95em; }
    .update-rate-selector select {
      background: #252525; color: #e0e0e0; border: 1px solid #555;
      padding: 8px 12px; border-radius: 8px; font-size: 0.9em; cursor: pointer;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }
    .update-rate-selector select:focus {
      outline: none; border-color: var(--accent-color, #d89904);
      box-shadow: 0 0 8px rgba(216, 153, 4, 0.3);
    }
    .sensor-error-message { /* Unverändert von vorher */
       color: #FFCC00; text-align: center; margin-top: 5px; margin-bottom: 15px;
       padding: 8px 10px; background-color: rgba(255, 204, 0, 0.1);
       border: 1px solid rgba(255, 204, 0, 0.2); border-radius: 8px;
       font-weight: 500; font-size: 0.9em; max-width: 600px;
       margin-left: auto; margin-right: auto;
    }
    .chart-error-box { /* Style für die Fehlermeldung, wenn Chart.js fehlt */
       margin: 20px auto; padding: 20px; max-width: 600px;
       background-color: rgba(255, 221, 221, 0.9); /* Heller Rotton */
       border: 1px solid #dc3545; /* Roter Rand */
       color: #333; /* Dunkle Schrift für Kontrast */
       border-radius: 8px; text-align: center;
    }
    .chart-error-box h2 { color: #dc3545; margin-bottom: 10px;}
    .chart-error-box a { color: #0056b3; text-decoration: underline; }

    @media (max-width: 600px) { /* Unverändert von vorher */
      h1.chart-title { font-size: 1.5rem; }
      .chart-container { width: 98%; padding: 10px; height: 60vh; }
      .update-rate-selector { flex-direction: column; gap: 5px; margin-bottom: 15px; }
      .sensor-error-message { width: 90%; font-size: 0.85em; }
      .chart-error-box { width: 90%; padding: 15px;}
    }
  </style>
)rawliteral";

  // Teil 2: Schließendes Head-Tag
  static const char chartsHtml_HeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

  // Teil 3: Body-Start, Titel, Update-Rate-Selektor, Placeholder für Sensorfehler, Chart-Container-Div
  static const char chartsHtml_BodyStartAndUI[] PROGMEM = R"rawliteral(
<body>
  <h1 class="chart-title">Live-Temperaturverlauf</h1>

  <div class="update-rate-selector">
    <label for="updateRate">Aktualisierungsrate:</label>
    <select id="updateRate">
      <option value="1000">1 Sekunde</option>
      <option value="2000">2 Sekunden</option>
      <option value="3000" selected>3 Sekunden</option>
      <option value="5000">5 Sekunden</option>
      <option value="10000">10 Sekunden</option>
      <option value="15000">15 Sekunden</option>
      <option value="30000">30 Sekunden</option>
    </select>
  </div>

  {SENSOR_ERROR_MESSAGES_PLACEHOLDER} <div class="chart-container"> )rawliteral"; // Div wird später geschlossen ODER Inhalt geändert

// Teil 4: Der eigentliche Chart.js Initialisierungs- und Update-Code
// (Der lange <script>...</script> Block aus der vorherigen Version)
static const char chartsHtml_ChartJSCode[] PROGMEM = R"rawliteral(
    const ctx = document.getElementById('combinedChart').getContext('2d');
    let currentSetpointDampf = 0; let currentSetpointWasser = 0;
    const wasserColor = '#00AEEF'; const dampfColor = '#F7941D';
    const setpointWasserColor = 'rgba(0, 174, 239, 0.7)';
    const setpointDampfColor = 'rgba(247, 148, 29, 0.7)';
    const gridColor = 'rgba(255, 255, 255, 0.1)'; const textColor = '#E0E0E0';
    const combinedChart = new Chart(ctx, {
      type: 'line', data: { labels: [],
        datasets: [
          { label: 'Wasser IST', data: [], borderColor: wasserColor, backgroundColor: 'rgba(0, 174, 239, 0.1)', borderWidth: 2, fill: 'start', tension: 0.4, pointRadius: 0, pointHoverRadius: 6, pointHitRadius: 10, pointHoverBackgroundColor: wasserColor },
          { label: 'Dampf IST', data: [], borderColor: dampfColor, backgroundColor: 'rgba(247, 148, 29, 0.1)', borderWidth: 2, fill: 'start', tension: 0.4, pointRadius: 0, pointHoverRadius: 6, pointHitRadius: 10, pointHoverBackgroundColor: dampfColor },
          { label: 'Wasser SOLL', data: [], borderColor: setpointWasserColor, borderWidth: 1.5, borderDash: [6, 3], fill: false, tension: 0.1, pointRadius: 0, pointHoverRadius: 0 },
          { label: 'Dampf SOLL', data: [], borderColor: setpointDampfColor, borderWidth: 1.5, borderDash: [6, 3], fill: false, tension: 0.1, pointRadius: 0, pointHoverRadius: 0 }
        ]
      }, options: { responsive: true, maintainAspectRatio: false, animation: false, interaction: { intersect: false, mode: 'index' },
        scales: {
          x: { type: 'category', title: { display: true, text: 'Uhrzeit', color: textColor, font: { size: 13, weight: '300' } }, ticks: { color: textColor, font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 8 }, grid: { color: gridColor, drawBorder: false } },
          y: { suggestedMin: 0, suggestedMax: 180, title: { display: true, text: 'Temperatur (°C)', color: textColor, font: { size: 13, weight: '300' } }, ticks: { color: textColor, font: { size: 11 }, stepSize: 20, callback: function(value, index, values) { return value + ' °C'; } }, grid: { color: gridColor, drawBorder: false } }
        }, plugins: {
          legend: { position: 'bottom', align: 'center', labels: { color: textColor, font: { size: 12 }, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 15, padding: 15 } },
          tooltip: { enabled: true, mode: 'index', intersect: false, backgroundColor: 'rgba(0, 0, 0, 0.85)', titleColor: '#ffffff', titleFont: { size: 13, weight: 'bold' }, bodyColor: '#dddddd', bodyFont: { size: 12 }, bodySpacing: 4, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 10, cornerRadius: 8, displayColors: true, boxPadding: 4,
            callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += context.parsed.y.toFixed(1) + ' °C'; } return label; } }
          }
        }
      }
    });
    let updateInterval = 3000; let intervalId = null; const MAX_DATA_POINTS = 100;
    function fetchData() {
      fetch('/Chart-Daten').then(response => { if (!response.ok) { console.error('Netzwerkantwort war nicht ok:', response.statusText); throw new Error('Network response was not ok'); } return response.json(); })
      .then(data => {
        if (data.setpointwasser !== undefined) currentSetpointWasser = data.setpointwasser; if (data.setpointdampf !== undefined) currentSetpointDampf = data.setpointdampf;
        const now = new Date(); const timeLabel = now.toLocaleTimeString('de-DE');
        const labels = combinedChart.data.labels; const datasets = combinedChart.data.datasets;
        labels.push(timeLabel);
        datasets[0].data.push(data.wasser); datasets[1].data.push(data.dampf); datasets[2].data.push(currentSetpointWasser); datasets[3].data.push(currentSetpointDampf);
        while (labels.length > MAX_DATA_POINTS) { labels.shift(); datasets.forEach(dataset => { dataset.data.shift(); }); }
        combinedChart.update('none');
      }).catch(err => { console.error("Fehler beim Abrufen der Daten:", err); });
    }
    function startFetching() { if (intervalId) { clearInterval(intervalId); intervalId = null; } fetchData(); intervalId = setInterval(fetchData, updateInterval); console.log(`Chart-Aktualisierung gestartet: Intervall ${updateInterval / 1000} Sekunden.`); }
    document.getElementById('updateRate').addEventListener('change', function(event) { const newInterval = parseInt(event.target.value, 10); if (!isNaN(newInterval) && newInterval > 0) { updateInterval = newInterval; startFetching(); } });
    startFetching();
)rawliteral";

  // Teil 5: Schließendes Body- und HTML-Tag
  static const char chartsHtml_BodyEnd[] PROGMEM = R"rawliteral(
</body>
</html>
)rawliteral";


  // --- Seitenaufbau starten ---
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), ""); // Header senden

  server.sendContent_P(chartsHtml_HeadStart);
  server.sendContent_P(commonStyle);          // Allgemeine Styles
  server.sendContent_P(chartsHtml_SpecificStyles); // Chart-spezifische Styles

  if (canLoadChartJs) {
    // Script-Tag für Chart.js einfügen (lokal oder CDN)
    String scriptTag = "<script src=\"";
    scriptTag += chartJsUrl;
    scriptTag += "\"></script>\n";
    server.sendContent(scriptTag);
  }
  // Head immer schließen
  server.sendContent_P(chartsHtml_HeadEnd);
  // Body-Tag senden
  server.sendContent("<body>\n");
  // Navigation immer einfügen
  server.sendContent_P(commonNav);
  // Haupt-Wrapper für den Inhalt öffnen
  server.sendContent("<main class=\"chart-page-content\">\n"); 

  // --- Body-Inhalt basierend auf Verfügbarkeit von Chart.js ---

  if (canLoadChartJs) {
    // Normaler Body mit Chart
    String bodyStart = FPSTR(chartsHtml_BodyStartAndUI); // Platzhalter holen

    // Platzhalter für Sensorfehler ersetzen (wie in der vorherigen handleRoot)
    String errorMessageHtml = "";
    if (wasserSensorError && dampfSensorError) {
        if (!demoModus) errorMessageHtml = "<div class=\"sensor-error-message\">Die Daten der Temperatursensoren sind derzeit nicht verf&uuml;gbar!<br>Bitte Temperatur-Sensoren pr&uuml;fen!</div>";
        else errorMessageHtml = "<div class=\"sensor-error-message\">Demo-Betrieb: Es sind keine Temperatur-Daten verf&uuml;gbar, bzw. keine Sensoren angeschlossen</div>";
    } else if (wasserSensorError) {
        errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Wasser nicht verf&uuml;gbar</div>";
    } else if (dampfSensorError) {
        errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Dampf nicht verf&uuml;gbar</div>";
    }
    bodyStart.replace(F("{SENSOR_ERROR_MESSAGES_PLACEHOLDER}"), errorMessageHtml);

    server.sendContent(bodyStart); // Modifizierten Body-Start senden

    // Canvas-Element für Chart.js einfügen
    server.sendContent("<canvas id=\"combinedChart\"></canvas>");
    // Chart-Container-Div schließen
    server.sendContent("</div>\n");

    // Chart Initialisierungs-JavaScript senden
    server.sendContent("<script>\n");
    server.sendContent_P(chartsHtml_ChartJSCode);
    server.sendContent("</script>\n");

  } else {
    // Fehlerfall: Chart.js nicht verfügbar
     server.sendContent("<body>\n"); // Body trotzdem öffnen
     server.sendContent("<h1 class='chart-title'>Live-Temperaturverlauf</h1>\n"); // Titel

     // Fehlermeldung anzeigen
     server.sendContent("<div class='chart-error-box'>"); // Eigene CSS-Klasse verwenden
     server.sendContent("<h2>Chart kann nicht angezeigt werden</h2>");
     server.sendContent("<p>Die Chart-Bibliothek (chart.umd.min.js) wurde weder im Dateisystem gefunden, noch besteht eine Internetverbindung zum Laden vom CDN.</p>");
     server.sendContent("<p>Bitte laden Sie die Datei <a href='https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js' target='_blank'>chart.umd.min.js</a> herunter und laden Sie sie über den <a href='/Dateimanager' target='_blank'>Dateimanager</a> hoch, oder verbinden Sie das Gerät mit dem Internet.</p>");
     server.sendContent("</div>\n");

     // Kein Chart-Container oder JS hier
  }
  server.sendContent("</main>\n");
  // Body und HTML immer schließen
  server.sendContent_P(chartsHtml_BodyEnd);

  // Leeren Chunk senden, um die Übertragung abzuschließen
  server.sendContent("");
}

/************************************************************************************
 * Profil-Funktionen (Anzeige) - Heap-Optimiert mit Chunked Sending & Leer-Check
 ************************************************************************************/

// --- PROGMEM Chunks für handleProfiles ---
// Kopfzeile bleibt gleich wie im Original `profilesHtml`
static const char profilesHtmlHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Profile</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
// </head> bleibt gleich wie im Original `profilesHtml2`
static const char profilesHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";

// Aufteilung von profilesHtml3 in Chunks
static const char profilesChunk_Intro[] PROGMEM = R"rawliteral(
<body>
  <h1>Temperaturprofile</h1>
  <form>
  Es k&ouml;nnen zwei unabh&auml;ngige Profile angelegt oder geladen werden.<br>
  <br>
  <b>In den Profilen werden gespeichert:</b><br>
  - Setpoint Wasser<br>
  - Setpoint Dampf<br>
  - Eco-Modus-Einstellungen<br>
  - Aufheizen f&uuml;r Dampf verz&ouml;gern<br>
  - Profilname<br>
  </form>
  <form action='/saveProfile1' method='POST'>
    <h3>Profil 1 (Name: )rawliteral"; // Endet vor dem 1. {PROFILE1NAME}
static const char profilesChunk_P1_Val[] PROGMEM = R"rawliteral()</h3>
    <label for='profileName1'>Neuen Profilnamen eingeben:</label>
    <input type='text' id='profileName1' name='profileName' value=')rawliteral"; // Endet vor dem 2. {PROFILE1NAME}
static const char profilesChunk_P1_EndForm[] PROGMEM = R"rawliteral(' maxlength='19'>
    <input type='submit' value='Speichern in Profil 1'>
  </form>
  <form action='/loadProfile1' method='POST'>
    <h3>Profil 1 laden</h3>
    Temperatureinstellungen aus <b>Profil 1</b> laden.<br>
    <input type='submit' value='Profil 1 laden'>
  </form>
  <form action='/saveProfile2' method='POST'>
    <h3>Profil 2 (Name: )rawliteral"; // Endet vor dem 1. {PROFILE2NAME}
static const char profilesChunk_P2_Val[] PROGMEM = R"rawliteral()</h3>
    <label for='profileName2'>Neuen Profilnamen eingeben:</label>
    <input type='text' id='profileName2' name='profileName' value=')rawliteral"; // Endet vor dem 2. {PROFILE2NAME}
static const char profilesChunk_P2_EndForm[] PROGMEM = R"rawliteral(' maxlength='19'>
    <input type='submit' value='Speichern in Profil 2'>
  </form>
  <form action='/loadProfile2' method='POST'>
    <h3>Profil 2 laden</h3>
    Temperatureinstellungen aus <b>Profil 2</b> laden<br>
    <input type='submit' value='Profil 2 laden'>
  </form>
</body>
</html>
)rawliteral";

// --- Überarbeitete Funktion handleProfiles ---
void handleProfiles() // URL: /Profile
{
  TemperatureProfile p1;
  TemperatureProfile p2;
  EEPROM.get(EEPROM_ADDR_PROFILE_1, p1);
  EEPROM.get(EEPROM_ADDR_PROFILE_2, p2);
  // Sicherstellen, dass Namen nullterminiert sind (wichtig!)
  p1.profileName[sizeof(p1.profileName)-1] = '\0';
  p2.profileName[sizeof(p2.profileName)-1] = '\0';

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Kopfzeile senden
  server.sendContent_P(profilesHtmlHead);
  server.sendContent_P(commonStyle);
  server.sendContent_P(profilesHtmlHeadEnd);
  server.sendContent_P(commonNav); // Navigation

  // Intro senden
  server.sendContent_P(profilesChunk_Intro);

  // Profil 1 Name (Anzeige im Titel) - Nur wenn nicht leer
  if (strlen(p1.profileName) > 0) {
    server.sendContent(p1.profileName);
  }
  // Nach 1. P1 Name bis value Attribut
  server.sendContent_P(profilesChunk_P1_Val);
  // Profil 1 Name (im Input-Feld) - Nur wenn nicht leer
  if (strlen(p1.profileName) > 0) {
    server.sendContent(p1.profileName);
  }
  // Rest des P1 Formulars und Start P2 Formular bis Titel
  server.sendContent_P(profilesChunk_P1_EndForm);

  // Profil 2 Name (Anzeige im Titel) - Nur wenn nicht leer
  if (strlen(p2.profileName) > 0) {
    server.sendContent(p2.profileName);
  }
  // Nach 1. P2 Name bis value Attribut
  server.sendContent_P(profilesChunk_P2_Val);
  // Profil 2 Name (im Input-Feld) - Nur wenn nicht leer
  if (strlen(p2.profileName) > 0) {
    server.sendContent(p2.profileName);
  }
  // Rest des P2 Formulars und der Seite
  server.sendContent_P(profilesChunk_P2_EndForm);

  // Letzter leerer Chunk zum Abschließen
  server.sendContent(F(""));
}

// Speichert aktuelles Setup in Profil 1 - URL angepasst
void handleSaveProfile1()
{
  TemperatureProfile p;
  EEPROM.get(EEPROM_ADDR_PROFILE_1, p);

  if (server.hasArg("profileName"))
  {
    strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName)-1);
    p.profileName[sizeof(p.profileName)-1] = '\0'; // Nullterminierung
  }

  // Wir verwenden die Werte aus dem EEPROM, damit nicht fälschlicherweise ECO-Werte genommen werden
  EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert: Lade Wert aus EEPROM
  EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf);   // Korrigiert: Lade Wert aus EEPROM

  p.ecoModeMinutes    = ecoModeMinutes;
  p.dynamicEcoActive  = dynamicEcoActive;
  p.dampfVerzoegerung = dampfVerzoegerung;

  EEPROM.put(EEPROM_ADDR_PROFILE_1, p);
  EEPROM.commit();

  server.sendHeader(F("Location"), F("/Profile")); // URL angepasst
  server.send(303);
}


// Lädt Profil 1 in die aktuellen Parameter - URL angepasst
void handleLoadProfile1()
{
  TemperatureProfile p;
  EEPROM.get(EEPROM_ADDR_PROFILE_1, p);

  SetpointWasser     = p.setpointWasser;
  SetpointDampf      = p.setpointDampf;
  ecoModeMinutes     = p.ecoModeMinutes;
  dynamicEcoActive   = p.dynamicEcoActive;
  dampfVerzoegerung  = p.dampfVerzoegerung;

  EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER,  SetpointWasser);
  EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF,   SetpointDampf);
  EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES,  ecoModeMinutes);
  EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
  EEPROM.put(EEPROM_ADDR_STEAM_DELAY,      dampfVerzoegerung);
  EEPROM.commit();

  // PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben
  // (Obwohl Profile aktuell keine PID-Parameter speichern, ist dies gute Praxis)
  // pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); // Wenn Profile Kp/Ki/Kd enthalten würden
  // pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);

  // Eco-Status zurücksetzen, falls nötig
  ecoModeAktiv = false;
  ecoModeActivatedTime = 0;


  server.sendHeader(F("Location"), F("/Profile")); // URL angepasst
  server.send(303);
}

// Speichert aktuelles Setup in Profil 2 - URL angepasst
void handleSaveProfile2()
{
  TemperatureProfile p;
  EEPROM.get(EEPROM_ADDR_PROFILE_2, p);

  if (server.hasArg("profileName"))
  {
    strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName)-1);
    p.profileName[sizeof(p.profileName)-1] = '\0'; // Nullterminierung
  }

  EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert
  EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf);   // Korrigiert

  p.ecoModeMinutes    = ecoModeMinutes;
  p.dynamicEcoActive  = dynamicEcoActive;
  p.dampfVerzoegerung = dampfVerzoegerung;

  EEPROM.put(EEPROM_ADDR_PROFILE_2, p);
  EEPROM.commit();

  server.sendHeader(F("Location"), F("/Profile")); // URL angepasst
  server.send(303);
}

// Lädt Profil 2 in die aktuellen Parameter - URL angepasst
void handleLoadProfile2()
{
  TemperatureProfile p;
  EEPROM.get(EEPROM_ADDR_PROFILE_2, p);

  SetpointWasser     = p.setpointWasser;
  SetpointDampf      = p.setpointDampf;
  ecoModeMinutes     = p.ecoModeMinutes;
  dynamicEcoActive   = p.dynamicEcoActive;
  dampfVerzoegerung  = p.dampfVerzoegerung;

  EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER,  SetpointWasser);
  EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF,   SetpointDampf);
  EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES,  ecoModeMinutes);
  EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
  EEPROM.put(EEPROM_ADDR_STEAM_DELAY,      dampfVerzoegerung);
  EEPROM.commit();

  // PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben
  // pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
  // pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);

  // Eco-Status zurücksetzen, falls nötig
  ecoModeAktiv = false;
  ecoModeActivatedTime = 0;

  server.sendHeader(F("Location"), F("/Profile")); // URL angepasst
  server.send(303);
  }

/************************************************************************************
 * Handler für die Dateimanager-Seite (/Dateimanager) - Heap-Optimiert
 ************************************************************************************/

// --- PROGMEM Chunks für handleFileManager ---
static const char fileMgrHead[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html lang='de'><head><meta charset='UTF-8'><title>Dateimanager</title>
<style>
body { font-family: sans-serif; background-color: #f0f0f0; margin: 15px; }
ul { list-style: none; padding: 0; }
li { margin-bottom: 8px; background: #fff; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; }
li span { flex-grow: 1; margin-right: 15px; }
.file-actions { display: flex; align-items: center; gap: 5px; }
a.button-like, input[type='submit'] { text-decoration: none; background: #eee; padding: 4px 8px; border: 1px solid #ccc; border-radius: 3px; color: #333; cursor: pointer; font-size: 0.9em; }
a.button-like:hover, input[type='submit']:hover { background: #ddd; }
form { margin: 0; padding: 0; border: none; display: inline; }
hr { margin: 20px 0; }
</style>
</head><body>
<h1>Dateimanager (LittleFS)</h1>
<h2>Vorhandene Dateien</h2>
<ul>
)rawliteral"; // Endet nach <ul>

static const char fileMgrListItemStart[] PROGMEM = R"rawliteral(<li><span>)rawliteral"; // Start eines Listeneintrags + Span
static const char fileMgrListItemMid1[] PROGMEM = R"rawliteral( ( )rawliteral"; // Nach Dateiname, vor Größe
static const char fileMgrListItemMid2[] PROGMEM = R"rawliteral( Bytes)</span><div class='file-actions'><a href='/Datei-Download?name=)rawliteral"; // Nach Größe, bis vor Download-Dateiname
static const char fileMgrListItemMid3[] PROGMEM = R"rawliteral(' class='button-like' download>Download</a><form action='/Datei-Entfernen' method='GET'><input type='hidden' name='name' value=')rawliteral"; // Nach Download-Dateiname, bis vor Delete-Dateiname
static const char fileMgrListItemEnd[] PROGMEM = R"rawliteral('><input type='submit' value='Entfernen'></form></div></li>)rawliteral"; // Nach Delete-Dateiname, bis Ende <li>

static const char fileMgrNoFiles[] PROGMEM = R"rawliteral(<li><i>Keine Dateien im Dateisystem gefunden.</i></li>)rawliteral";
static const char fileMgrListError[] PROGMEM = R"rawliteral(<li><i>Fehler beim Auflisten der Dateien.</i></li>)rawliteral";
static const char fileMgrOpenError[] PROGMEM = R"rawliteral(<li><i>Fehler beim Öffnen des Root-Verzeichnisses.</i></li>)rawliteral";

static const char fileMgrListEnd[] PROGMEM = R"rawliteral(</ul><hr>)rawliteral"; // Ende der Liste + Trennlinie

static const char fileMgrUploadForm[] PROGMEM = R"rawliteral(
<h2>Datei hochladen</h2>
<form method='POST' action='/Datei-Upload' enctype='multipart/form-data'>
<input type='file' name='upload' required><br><br>
<input type='submit' value='Datei Hochladen'>
</form>
)rawliteral";

static const char fileMgrInfoStart[] PROGMEM = R"rawliteral(<hr><h2>Info</h2>)rawliteral";
static const char fileMgrInfoTotal[] PROGMEM = R"rawliteral(Gesamtspeicher: )rawliteral";
static const char fileMgrInfoUsed[] PROGMEM = R"rawliteral( Bytes<br>Belegt: )rawliteral";
static const char fileMgrInfoFree[] PROGMEM = R"rawliteral( Bytes<br>Frei: )rawliteral";
static const char fileMgrInfoEnd[] PROGMEM = R"rawliteral( Bytes<br>)rawliteral";
static const char fileMgrInfoError[] PROGMEM = R"rawliteral(Fehler beim Abrufen der Dateisystem-Informationen.<br>)rawliteral";

static const char fileMgrBodyEnd[] PROGMEM = R"rawliteral(</body></html>)rawliteral";


// --- Überarbeitete Funktion handleFileManager ---
void handleFileManager() {
  Serial.println("Anfrage für /Dateimanager empfangen (Heap-optimiert).");
  char buffer[20]; // Puffer für Zahlenumwandlung (Dateigröße etc.)

  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, F("text/html"), "");

  // Head, Style, Body-Start, Titel, Start der Liste
  server.sendContent_P(fileMgrHead);

  // --- Dateien auflisten ---
  int fileCount = 0;
  Dir dir = LittleFS.openDir("/");
  while (dir.next()) {
    fileCount++;
    String currentFileName = dir.fileName(); // Temporäres String-Objekt holen
    size_t currentFileSize = dir.fileSize();

    server.sendContent_P(fileMgrListItemStart); // <li><span>
    if (currentFileName.length() > 0) {
        server.sendContent(currentFileName.c_str()); // Dateiname senden (als c_str)
    }
    server.sendContent_P(fileMgrListItemMid1); //  (
    snprintf(buffer, sizeof(buffer), "%d", currentFileSize); // Dateigröße formatieren
    server.sendContent(buffer); // Dateigröße senden
    server.sendContent_P(fileMgrListItemMid2); // Bytes)</span><div ... href='...?name=
    if (currentFileName.length() > 0) {
        server.sendContent(currentFileName.c_str()); // Dateiname für Download senden
    }
    server.sendContent_P(fileMgrListItemMid3); // ' class...>...<input value='
     if (currentFileName.length() > 0) {
        server.sendContent(currentFileName.c_str()); // Dateiname für Delete senden
    }
    server.sendContent_P(fileMgrListItemEnd); // '><input ...></div></li>
  }

  // --- Meldung, wenn keine Dateien gefunden wurden ---
  if (fileCount == 0) {
    // Prüfen ob wirklich leer oder nur Fehler beim Iterieren
    File root = LittleFS.open("/", "r");
    if (root) {
      File file = root.openNextFile();
      if (!file) {
        server.sendContent_P(fileMgrNoFiles); // Keine Dateien gefunden
      } else {
        server.sendContent_P(fileMgrListError); // Fehler beim Auflisten
        file.close();
      }
      root.close();
    } else {
       server.sendContent_P(fileMgrOpenError); // Fehler beim Öffnen von "/"
    }
  }

  // Ende der Liste + Trennlinie
  server.sendContent_P(fileMgrListEnd);

  // --- Datei-Upload-Formular ---
  server.sendContent_P(fileMgrUploadForm);

  // --- Dateisystem-Informationen (Optional) ---
  server.sendContent_P(fileMgrInfoStart); // <hr><h2>Info</h2>
  FSInfo fs_info;
  if (LittleFS.info(fs_info)) {
     server.sendContent_P(fileMgrInfoTotal); // Gesamtspeicher:
     snprintf(buffer, sizeof(buffer), "%u", fs_info.totalBytes); server.sendContent(buffer);
     server.sendContent_P(fileMgrInfoUsed); // Bytes<br>Belegt:
     snprintf(buffer, sizeof(buffer), "%u", fs_info.usedBytes); server.sendContent(buffer);
     server.sendContent_P(fileMgrInfoFree); // Bytes<br>Frei:
     snprintf(buffer, sizeof(buffer), "%u", fs_info.totalBytes - fs_info.usedBytes); server.sendContent(buffer);
     server.sendContent_P(fileMgrInfoEnd); // Bytes<br>
  } else {
     server.sendContent_P(fileMgrInfoError); // Fehlermeldung
  }

  // Ende Body/HTML
  server.sendContent_P(fileMgrBodyEnd);

  // Abschluss-Chunk
  server.sendContent(F(""));
}

/************************************************************************************
 * Handler während des Datei-Uploads
 ************************************************************************************/
void handleFileUpload() {
  HTTPUpload& upload = server.upload();
  switch (upload.status) {
    case UPLOAD_FILE_START: {
      String filename = upload.filename;
      // Basisschutz gegen Pfadmanipulation (obwohl LittleFS das meiste handhaben sollte)
      if (filename.indexOf('/') != -1 && filename.indexOf('/') == 0) {
         // Einfach den Dateinamen nehmen, wenn Pfade enthalten sind
         filename = filename.substring(filename.lastIndexOf('/') + 1);
      }
      filename = "/" + filename; // Immer im Root speichern

      Serial.printf("Upload Start: %s\n", filename.c_str());
      fsUploadFile = LittleFS.open(filename, "w"); // Öffnen zum Schreiben (überschreibt ggf. existierende Datei)
      if (!fsUploadFile) {
        Serial.println("Konnte Datei für Upload nicht erstellen/öffnen!");
        // Hier könnten wir theoretisch den Upload abbrechen, ist aber komplex
      }
      break;
    }
    case UPLOAD_FILE_WRITE: {
      if (fsUploadFile) {
        size_t bytesWritten = fsUploadFile.write(upload.buf, upload.currentSize);
        if (bytesWritten != upload.currentSize) {
          Serial.printf("Fehler beim Schreiben! Erwartet: %d, Geschrieben: %d\n", upload.currentSize, bytesWritten);
          // Datei schließen und löschen?
          fsUploadFile.close();
          LittleFS.remove(fsUploadFile.name()); // Versuch, unvollständige Datei zu löschen
        } else {
           // Serial.printf("Schreibe Chunk: %d Bytes\n", bytesWritten); // Debug
        }
      } else {
         Serial.println("Upload WRITE, aber keine Datei geöffnet!");
      }
      break;
    }
    case UPLOAD_FILE_END: {
      if (fsUploadFile) {
        fsUploadFile.close();
        Serial.printf("Upload Ende. Gesamtgröße: %d Bytes\n", upload.totalSize);
      } else {
         Serial.println("Upload END, aber keine Datei geöffnet!");
      }
      break;
    }
    case UPLOAD_FILE_ABORTED: {
       Serial.println("Upload abgebrochen!");
       if (fsUploadFile) {
           fsUploadFile.close();
           // Optional: Versuchen, die (möglicherweise unvollständige) Datei zu löschen
           Serial.printf("Versuche, abgebrochene Upload-Datei zu löschen: %s\n", fsUploadFile.name());
           if (LittleFS.exists(fsUploadFile.name())) {
              if (LittleFS.remove(fsUploadFile.name())) {
                 Serial.println("Abgebrochene Datei gelöscht.");
              } else {
                 Serial.println("Fehler beim Löschen der abgebrochenen Datei.");
              }
           }
       }
       break;
    }
  }
}

/************************************************************************************
 * Handler NACHDEM der Upload abgeschlossen (oder fehlgeschlagen) ist
 ************************************************************************************/
void handleFileUploaded() {
  // Einfach zurück zum Dateimanager leiten.
  // Eine Statusmeldung wäre besser, aber würde mehr Aufwand bedeuten (z.B. über URL-Parameter).
  server.sendHeader("Location", "/Dateimanager");
  server.send(303); // 303 See Other - Korrekt für Redirect nach POST
}

/************************************************************************************
 * Handler zum Herunterladen einer Datei
 ************************************************************************************/
void handleFileDownload() {
  if (!server.hasArg("name") || server.arg("name") == NULL || server.arg("name") == "") {
    server.send(400, "text/plain", "FEHLER: Dateiname zum Herunterladen fehlt!");
    return;
  }

  String filenameParam = server.arg("name");
  String filePath = filenameParam; // Kopieren, um ggf. zu modifizieren

  // Sicherstellen, dass der Pfad mit '/' beginnt (Basis-Sicherheit)
  if (!filePath.startsWith("/")) {
    filePath = "/" + filePath;
  }

  // Verhindern, dass Root selbst angefordert wird
  if (filePath == "/") {
       server.send(400, "text/plain", "FEHLER: Root-Verzeichnis kann nicht heruntergeladen werden.");
       return;
  }

  Serial.printf("Download-Anfrage für: %s (Parameter: %s)\n", filePath.c_str(), filenameParam.c_str());

  if (LittleFS.exists(filePath)) {
    File file = LittleFS.open(filePath, "r");
    if (file) {
      // Setze den Content-Type auf 'application/octet-stream',
      // um den Browser zum Download zu zwingen, anstatt die Datei anzuzeigen.
      // Alternativ könntest du versuchen, den Typ anhand der Endung zu erraten.
      String contentType = "application/octet-stream";

      // Setze den Content-Disposition Header, um den Dateinamen vorzuschlagen
      // filenameParam enthält hier nur den Dateinamen ohne führenden Slash
      server.sendHeader("Content-Disposition", "attachment; filename=\"" + filenameParam + "\"");

      // Sende die Datei als Stream (effizient für größere Dateien)
      // sendHeader("Content-Length", String(file.size())); // Optional aber gut für Fortschrittsanzeige
      size_t sentBytes = server.streamFile(file, contentType);
      file.close();
      Serial.printf("Datei '%s' (%d Bytes) gesendet.\n", filePath.c_str(), sentBytes);
      return; // Wichtig: Funktion nach dem Senden beenden
    } else {
      Serial.println("Fehler beim Öffnen der Datei für den Download!");
      server.send(500, "text/plain", "FEHLER: Datei konnte nicht geöffnet werden.");
    }
  } else {
    Serial.println("Datei zum Herunterladen nicht gefunden!");
    server.send(404, "text/plain", "FEHLER: Datei nicht gefunden.");
  }
}

/************************************************************************************
 * Handler zum Löschen einer Datei
 ************************************************************************************/
void handleFileDelete() {
  if (!server.hasArg("name") || server.arg("name") == NULL || server.arg("name") == "") {
    server.send(400, "text/plain", "FEHLER: Dateiname zum Löschen fehlt!");
    return;
  }

  String filename = server.arg("name");
  // Sicherstellen, dass der Pfad relativ zum Root ist (Basis-Sicherheit)
   if (!filename.startsWith("/")) {
     filename = "/" + filename;
   }
   // Verhindere das Löschen des Root-Verzeichnisses selbst (obwohl LittleFS.remove("/") fehlschlagen sollte)
   if (filename == "/") {
       server.send(400, "text/plain", "FEHLER: Root-Verzeichnis kann nicht gelöscht werden.");
       return;
   }


  Serial.printf("Versuche Datei zu löschen: %s\n", filename.c_str());

  if (LittleFS.exists(filename)) {
      if (LittleFS.remove(filename)) {
        Serial.println("Datei erfolgreich gelöscht.");
        // Redirect zurück zum Dateimanager
        server.sendHeader("Location", "/Dateimanager");
        server.send(303);
      } else {
        Serial.println("Fehler beim Löschen der Datei!");
        server.send(500, "text/plain", "FEHLER: Datei konnte nicht gelöscht werden.");
      }
  } else {
      Serial.println("Datei zum Löschen nicht gefunden!");
      server.send(404, "text/plain", "FEHLER: Datei nicht gefunden.");
  }
}

/************************************************************************************
 * Handler zum Ausliefern der lokalen Chart.js Datei aus LittleFS
 ************************************************************************************/
void handleServeChartJs() {
  const char* chartJsPath = "/chart.umd.min.js";
  if (LittleFS.exists(chartJsPath)) {
    File file = LittleFS.open(chartJsPath, "r");
    if (file) {
      server.streamFile(file, "text/javascript"); // Effizientes Senden
      file.close();
      Serial.println("Lokale chart.umd.min.js ausgeliefert.");
      return; // Wichtig: Beenden nach erfolgreichem Senden
    } else {
      Serial.println("Fehler beim Öffnen der lokalen chart.umd.min.js!");
    }
  } else {
     Serial.println("Angeforderte lokale chart.umd.min.js nicht gefunden!");
  }
  // Wenn hier angekommen, ist etwas schiefgelaufen
  server.send(404, "text/plain", "Chart.js nicht gefunden");
}

/************************************************************************************
 * Handler für den Firmware-Upload-Prozess (während des Uploads)
 ************************************************************************************/
void handleFirmwareUploadProgress() {
  // --- Dieser Code war vorher die zweite Lambda-Funktion ---
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START)
  {
    Serial.printf("Update gestartet: %s\n", upload.filename.c_str());
    passwordFound = false; // Reset für jede neue Datei
    passMatchPos  = 0;

    // Freien Speicher für Update prüfen und Update starten
    // (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000 berechnet die Startadresse
    if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000))
    {
      Update.printError(Serial);
    }
  }
  else if (upload.status == UPLOAD_FILE_WRITE)
  {
    // Prüfen auf Firmware-Passwort während des Schreibens
    // (Diese Logik bleibt wie im Original)
    for (size_t i = 0; i < upload.currentSize; i++)
    {
      char c = (char)upload.buf[i];
      if (!passwordFound) { // Nur prüfen, wenn noch nicht gefunden
           if (c == FIRMWARE_PASSWORD[passMatchPos]) {
              passMatchPos++;
              if (passMatchPos == passLength) {
                  passwordFound = true;
                  Serial.println("Firmware-Kennwort gefunden!");
              }
           } else {
               passMatchPos = (c == FIRMWARE_PASSWORD[0]) ? 1 : 0;
           }
      }
    }

    // Daten in den Flash schreiben
    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
    {
      Update.printError(Serial);
    }
  }
  else if (upload.status == UPLOAD_FILE_END)
  {
    // Upload abgeschlossen
    if (!passwordFound) // Wurde das Kennwort während des Uploads gefunden?
    {
      Serial.println("Kennwort nicht in Firmware gefunden! Update wird abgebrochen.");
      Update.end(false); // Update abbrechen (false = kein Erfolg)
    }
    else
    {
      // Kennwort gefunden, versuche Update abzuschließen
      if (Update.end(true)) // Update erfolgreich beenden (true = Erfolg)
      {
        Serial.printf("Update erfolgreich: %u Bytes\n", upload.totalSize);
      }
      else
      {
        Update.printError(Serial); // Fehler beim Abschluss des Updates
      }
    }
    // Variablen für nächsten Upload zurücksetzen (kann auch hier erfolgen)
    // passwordFound = false; // Wird bereits in UPLOAD_FILE_START zurückgesetzt
    // passMatchPos = 0;
  }
  else if (upload.status == UPLOAD_FILE_ABORTED)
  {
    // Upload vom Client abgebrochen
    Update.end(false); // Sicherstellen, dass Update als nicht erfolgreich markiert wird
    Serial.println(F("Update abgebrochen (vom Client)."));
    passwordFound = false; // Status zurücksetzen
    passMatchPos = 0;
  }
  yield(); // Wichtig, um den ESP am Laufen zu halten
  // --- Ende des ursprünglichen Lambda-Codes ---
}

/************************************************************************************
 * Handler für die Antwort nach einem Firmware-Update - Heap-Optimiert
 ************************************************************************************/

// PROGMEM Chunks für die Seite
static const char updateDoneHtmlStart[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>Firmware-Update</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char updateDoneHtmlHeadEnd[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char updateDoneHtmlBodyStart[] PROGMEM = R"rawliteral(
<head>
<meta http-equiv="refresh" content="30;url=/" />
<script>
    setTimeout(function() {
        window.location.href = "/";
    }, 30000);
</script>
<body>
    <h1>)rawliteral"; // Endet vor {status_message}
static const char updateDoneHtmlBodyEnd[] PROGMEM = R"rawliteral(</h1>
    <form><p>Weiterleitung zur Startseite in 30 Sekunden ...</p></form>
</body>
</html>
)rawliteral"; // Beginnt nach {status_message}

// --- Neue Funktion ---
void handleFirmwareUpdateResponse() {
    String statusMessage; // Status wird hier ermittelt
    if (!passwordFound) {
      statusMessage = F("Update abgebrochen! Die Firmware konnte nicht validiert werden.");
    } else {
      if (Update.hasError()) {
        statusMessage = F("Update fehlgeschlagen!");
      } else {
        statusMessage = F("Update erfolgreich!");
      }
    }

    // Seite senden
    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    server.send(200, F("text/html"), "");

    server.sendContent_P(updateDoneHtmlStart);
    server.sendContent_P(commonStyle);
    server.sendContent_P(updateDoneHtmlHeadEnd);
    server.sendContent_P(commonNav);
    server.sendContent_P(updateDoneHtmlBodyStart);

    // Status senden (ist ein F()-Makro oder String)
    if (statusMessage.length() > 0) {
        server.sendContent(statusMessage);
    }

    server.sendContent_P(updateDoneHtmlBodyEnd);
    server.sendContent(F("")); // Abschluss-Chunk

    // Neustart-Logik
    delay(1000); // Warte kurz, damit der Browser die Seite rendern kann
    ESP.restart();
}